Add new intent/method for cropping and setting wallpapers
Also, create a system fallback WallpaperCropper Bug: 4225598 Change-Id: I6bc6d5a3bb3df1dc00f3db701978aa172020c568
This commit is contained in:
@@ -4364,6 +4364,7 @@ package android.app {
|
||||
method public void clear() throws java.io.IOException;
|
||||
method public void clearWallpaperOffsets(android.os.IBinder);
|
||||
method public void forgetLoadedWallpaper();
|
||||
method public android.content.Intent getCropAndSetWallpaperIntent(android.net.Uri);
|
||||
method public int getDesiredMinimumHeight();
|
||||
method public int getDesiredMinimumWidth();
|
||||
method public android.graphics.drawable.Drawable getDrawable();
|
||||
@@ -4381,6 +4382,7 @@ package android.app {
|
||||
method public void setWallpaperOffsets(android.os.IBinder, float, float);
|
||||
method public void suggestDesiredDimensions(int, int);
|
||||
field public static final java.lang.String ACTION_CHANGE_LIVE_WALLPAPER = "android.service.wallpaper.CHANGE_LIVE_WALLPAPER";
|
||||
field public static final java.lang.String ACTION_CROP_AND_SET_WALLPAPER = "android.service.wallpaper.CROP_AND_SET_WALLPAPER";
|
||||
field public static final java.lang.String ACTION_LIVE_WALLPAPER_CHOOSER = "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER";
|
||||
field public static final java.lang.String COMMAND_DROP = "android.home.drop";
|
||||
field public static final java.lang.String COMMAND_SECONDARY_TAP = "android.wallpaper.secondaryTap";
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
|
||||
package android.app;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
@@ -30,7 +33,7 @@ import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Binder;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
@@ -41,13 +44,13 @@ import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.ViewRootImpl;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides access to the system wallpaper. With WallpaperManager, you can
|
||||
@@ -61,6 +64,15 @@ public class WallpaperManager {
|
||||
private float mWallpaperXStep = -1;
|
||||
private float mWallpaperYStep = -1;
|
||||
|
||||
/**
|
||||
* Activity Action: Show settings for choosing wallpaper. Do not use directly to construct
|
||||
* an intent; instead, use {@link #getCropAndSetWallpaperIntent}.
|
||||
* <p>Input: {@link Intent#getData} is the URI of the image to crop and set as wallpaper.
|
||||
* <p>Output: RESULT_OK if user decided to crop/set the wallpaper, RESULT_CANCEL otherwise
|
||||
*/
|
||||
public static final String ACTION_CROP_AND_SET_WALLPAPER =
|
||||
"android.service.wallpaper.CROP_AND_SET_WALLPAPER";
|
||||
|
||||
/**
|
||||
* Launch an activity for the user to pick the current global live
|
||||
* wallpaper.
|
||||
@@ -463,7 +475,39 @@ public class WallpaperManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets an Intent that will launch an activity that crops the given
|
||||
* image and sets the device's wallpaper. If there is a default HOME activity
|
||||
* that supports cropping wallpapers, it will be preferred as the default.
|
||||
* Use this method instead of directly creating a {@link Intent#CROP_AND_SET_WALLPAPER}
|
||||
* intent.
|
||||
*/
|
||||
public Intent getCropAndSetWallpaperIntent(Uri imageUri) {
|
||||
final PackageManager packageManager = mContext.getPackageManager();
|
||||
Intent cropAndSetWallpaperIntent =
|
||||
new Intent(ACTION_CROP_AND_SET_WALLPAPER, imageUri);
|
||||
cropAndSetWallpaperIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
// Find out if the default HOME activity supports CROP_AND_SET_WALLPAPER
|
||||
Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
|
||||
ResolveInfo resolvedHome = packageManager.resolveActivity(homeIntent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY);
|
||||
if (resolvedHome != null) {
|
||||
cropAndSetWallpaperIntent.setPackage(resolvedHome.activityInfo.packageName);
|
||||
|
||||
List<ResolveInfo> cropAppList = packageManager.queryIntentActivities(
|
||||
cropAndSetWallpaperIntent, 0);
|
||||
if (cropAppList.size() > 0) {
|
||||
return cropAndSetWallpaperIntent;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback crop activity
|
||||
cropAndSetWallpaperIntent.setPackage("com.android.wallpapercropper");
|
||||
return cropAndSetWallpaperIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current system wallpaper to the bitmap in the given resource.
|
||||
* The resource is opened as a raw data stream and copied into the
|
||||
|
||||
19
packages/WallpaperCropper/Android.mk
Normal file
19
packages/WallpaperCropper/Android.mk
Normal file
@@ -0,0 +1,19 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := telephony-common
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
|
||||
|
||||
LOCAL_PACKAGE_NAME := WallpaperCropper
|
||||
LOCAL_CERTIFICATE := platform
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
|
||||
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
include $(call all-makefiles-under,$(LOCAL_PATH))
|
||||
19
packages/WallpaperCropper/AndroidManifest.xml
Normal file
19
packages/WallpaperCropper/AndroidManifest.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.wallpapercropper" >
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
|
||||
|
||||
<application android:requiredForAllUsers="true">
|
||||
<activity
|
||||
android:name="WallpaperCropActivity"
|
||||
android:theme="@style/Theme.WallpaperCropper"
|
||||
android:label="@string/crop_wallpaper"
|
||||
android:finishOnCloseSystemDialogs="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.wallpaper.CROP_AND_SET_WALLPAPER" />
|
||||
<data android:mimeType="image/*" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
0
packages/WallpaperCropper/proguard.flags
Normal file
0
packages/WallpaperCropper/proguard.flags
Normal file
BIN
packages/WallpaperCropper/res/drawable-hdpi/ic_actionbar_accept.png
Executable file
BIN
packages/WallpaperCropper/res/drawable-hdpi/ic_actionbar_accept.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/WallpaperCropper/res/drawable-mdpi/ic_actionbar_accept.png
Executable file
BIN
packages/WallpaperCropper/res/drawable-mdpi/ic_actionbar_accept.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/WallpaperCropper/res/drawable-xhdpi/ic_actionbar_accept.png
Executable file
BIN
packages/WallpaperCropper/res/drawable-xhdpi/ic_actionbar_accept.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
**
|
||||
** Copyright 2013, 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.
|
||||
*/
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="?android:actionButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
<TextView style="?android:actionBarTabTextStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingRight="20dp"
|
||||
android:drawableLeft="@drawable/ic_actionbar_accept"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/wallpaper_instructions" />
|
||||
</FrameLayout>
|
||||
39
packages/WallpaperCropper/res/layout/wallpaper_cropper.xml
Normal file
39
packages/WallpaperCropper/res/layout/wallpaper_cropper.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
**
|
||||
** Copyright 2013, 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.
|
||||
*/
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/wallpaper_cropper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<com.android.wallpapercropper.CropView
|
||||
android:id="@+id/cropView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
style="@android:style/Widget.Holo.ProgressBar.Large"
|
||||
android:visibility="invisible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateOnly="true"
|
||||
android:background="@android:color/transparent" />
|
||||
</RelativeLayout>
|
||||
20
packages/WallpaperCropper/res/values/strings.xml
Normal file
20
packages/WallpaperCropper/res/values/strings.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2013 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.
|
||||
-->
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crop_wallpaper">Crop wallpaper</string>
|
||||
<!-- Button label on Wallpaper picker screen; user selects this button to set a specific wallpaper -->
|
||||
<string name="wallpaper_instructions">Set wallpaper</string>
|
||||
</resources>
|
||||
28
packages/WallpaperCropper/res/values/styles.xml
Normal file
28
packages/WallpaperCropper/res/values/styles.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2013 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<style name="Theme.WallpaperCropper" parent="@android:style/Theme.Holo">
|
||||
<item name="android:actionBarStyle">@style/WallpaperCropperActionBar</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowActionBarOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="WallpaperCropperActionBar" parent="android:style/Widget.Holo.ActionBar">
|
||||
<item name="android:displayOptions">showCustom</item>
|
||||
<item name="android:background">#88000000</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.common;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
import android.util.FloatMath;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class BitmapUtils {
|
||||
private static final String TAG = "BitmapUtils";
|
||||
private static final int DEFAULT_JPEG_QUALITY = 90;
|
||||
public static final int UNCONSTRAINED = -1;
|
||||
|
||||
private BitmapUtils(){}
|
||||
|
||||
/*
|
||||
* Compute the sample size as a function of minSideLength
|
||||
* and maxNumOfPixels.
|
||||
* minSideLength is used to specify that minimal width or height of a
|
||||
* bitmap.
|
||||
* maxNumOfPixels is used to specify the maximal size in pixels that is
|
||||
* tolerable in terms of memory usage.
|
||||
*
|
||||
* The function returns a sample size based on the constraints.
|
||||
* Both size and minSideLength can be passed in as UNCONSTRAINED,
|
||||
* which indicates no care of the corresponding constraint.
|
||||
* The functions prefers returning a sample size that
|
||||
* generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
|
||||
*
|
||||
* Also, the function rounds up the sample size to a power of 2 or multiple
|
||||
* of 8 because BitmapFactory only honors sample size this way.
|
||||
* For example, BitmapFactory downsamples an image by 2 even though the
|
||||
* request is 3. So we round up the sample size to avoid OOM.
|
||||
*/
|
||||
public static int computeSampleSize(int width, int height,
|
||||
int minSideLength, int maxNumOfPixels) {
|
||||
int initialSize = computeInitialSampleSize(
|
||||
width, height, minSideLength, maxNumOfPixels);
|
||||
|
||||
return initialSize <= 8
|
||||
? Utils.nextPowerOf2(initialSize)
|
||||
: (initialSize + 7) / 8 * 8;
|
||||
}
|
||||
|
||||
private static int computeInitialSampleSize(int w, int h,
|
||||
int minSideLength, int maxNumOfPixels) {
|
||||
if (maxNumOfPixels == UNCONSTRAINED
|
||||
&& minSideLength == UNCONSTRAINED) return 1;
|
||||
|
||||
int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
|
||||
(int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels));
|
||||
|
||||
if (minSideLength == UNCONSTRAINED) {
|
||||
return lowerBound;
|
||||
} else {
|
||||
int sampleSize = Math.min(w / minSideLength, h / minSideLength);
|
||||
return Math.max(sampleSize, lowerBound);
|
||||
}
|
||||
}
|
||||
|
||||
// This computes a sample size which makes the longer side at least
|
||||
// minSideLength long. If that's not possible, return 1.
|
||||
public static int computeSampleSizeLarger(int w, int h,
|
||||
int minSideLength) {
|
||||
int initialSize = Math.max(w / minSideLength, h / minSideLength);
|
||||
if (initialSize <= 1) return 1;
|
||||
|
||||
return initialSize <= 8
|
||||
? Utils.prevPowerOf2(initialSize)
|
||||
: initialSize / 8 * 8;
|
||||
}
|
||||
|
||||
// Find the min x that 1 / x >= scale
|
||||
public static int computeSampleSizeLarger(float scale) {
|
||||
int initialSize = (int) FloatMath.floor(1f / scale);
|
||||
if (initialSize <= 1) return 1;
|
||||
|
||||
return initialSize <= 8
|
||||
? Utils.prevPowerOf2(initialSize)
|
||||
: initialSize / 8 * 8;
|
||||
}
|
||||
|
||||
// Find the max x that 1 / x <= scale.
|
||||
public static int computeSampleSize(float scale) {
|
||||
Utils.assertTrue(scale > 0);
|
||||
int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale));
|
||||
return initialSize <= 8
|
||||
? Utils.nextPowerOf2(initialSize)
|
||||
: (initialSize + 7) / 8 * 8;
|
||||
}
|
||||
|
||||
public static Bitmap resizeBitmapByScale(
|
||||
Bitmap bitmap, float scale, boolean recycle) {
|
||||
int width = Math.round(bitmap.getWidth() * scale);
|
||||
int height = Math.round(bitmap.getHeight() * scale);
|
||||
if (width == bitmap.getWidth()
|
||||
&& height == bitmap.getHeight()) return bitmap;
|
||||
Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
|
||||
Canvas canvas = new Canvas(target);
|
||||
canvas.scale(scale, scale);
|
||||
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
if (recycle) bitmap.recycle();
|
||||
return target;
|
||||
}
|
||||
|
||||
private static Bitmap.Config getConfig(Bitmap bitmap) {
|
||||
Bitmap.Config config = bitmap.getConfig();
|
||||
if (config == null) {
|
||||
config = Bitmap.Config.ARGB_8888;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Bitmap resizeDownBySideLength(
|
||||
Bitmap bitmap, int maxLength, boolean recycle) {
|
||||
int srcWidth = bitmap.getWidth();
|
||||
int srcHeight = bitmap.getHeight();
|
||||
float scale = Math.min(
|
||||
(float) maxLength / srcWidth, (float) maxLength / srcHeight);
|
||||
if (scale >= 1.0f) return bitmap;
|
||||
return resizeBitmapByScale(bitmap, scale, recycle);
|
||||
}
|
||||
|
||||
public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
|
||||
int w = bitmap.getWidth();
|
||||
int h = bitmap.getHeight();
|
||||
if (w == size && h == size) return bitmap;
|
||||
|
||||
// scale the image so that the shorter side equals to the target;
|
||||
// the longer side will be center-cropped.
|
||||
float scale = (float) size / Math.min(w, h);
|
||||
|
||||
Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
|
||||
int width = Math.round(scale * bitmap.getWidth());
|
||||
int height = Math.round(scale * bitmap.getHeight());
|
||||
Canvas canvas = new Canvas(target);
|
||||
canvas.translate((size - width) / 2f, (size - height) / 2f);
|
||||
canvas.scale(scale, scale);
|
||||
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
if (recycle) bitmap.recycle();
|
||||
return target;
|
||||
}
|
||||
|
||||
public static void recycleSilently(Bitmap bitmap) {
|
||||
if (bitmap == null) return;
|
||||
try {
|
||||
bitmap.recycle();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "unable recycle bitmap", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
|
||||
if (rotation == 0) return source;
|
||||
int w = source.getWidth();
|
||||
int h = source.getHeight();
|
||||
Matrix m = new Matrix();
|
||||
m.postRotate(rotation);
|
||||
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
|
||||
if (recycle) source.recycle();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Bitmap createVideoThumbnail(String filePath) {
|
||||
// MediaMetadataRetriever is available on API Level 8
|
||||
// but is hidden until API Level 10
|
||||
Class<?> clazz = null;
|
||||
Object instance = null;
|
||||
try {
|
||||
clazz = Class.forName("android.media.MediaMetadataRetriever");
|
||||
instance = clazz.newInstance();
|
||||
|
||||
Method method = clazz.getMethod("setDataSource", String.class);
|
||||
method.invoke(instance, filePath);
|
||||
|
||||
// The method name changes between API Level 9 and 10.
|
||||
if (Build.VERSION.SDK_INT <= 9) {
|
||||
return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
|
||||
} else {
|
||||
byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
|
||||
if (data != null) {
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
if (bitmap != null) return bitmap;
|
||||
}
|
||||
return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// Assume this is a corrupt video file
|
||||
} catch (RuntimeException ex) {
|
||||
// Assume this is a corrupt video file.
|
||||
} catch (InstantiationException e) {
|
||||
Log.e(TAG, "createVideoThumbnail", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.e(TAG, "createVideoThumbnail", e);
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "createVideoThumbnail", e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.e(TAG, "createVideoThumbnail", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.e(TAG, "createVideoThumbnail", e);
|
||||
} finally {
|
||||
try {
|
||||
if (instance != null) {
|
||||
clazz.getMethod("release").invoke(instance);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] compressToBytes(Bitmap bitmap) {
|
||||
return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
|
||||
}
|
||||
|
||||
public static byte[] compressToBytes(Bitmap bitmap, int quality) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
|
||||
bitmap.compress(CompressFormat.JPEG, quality, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static boolean isSupportedByRegionDecoder(String mimeType) {
|
||||
if (mimeType == null) return false;
|
||||
mimeType = mimeType.toLowerCase();
|
||||
return mimeType.startsWith("image/") &&
|
||||
(!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
|
||||
}
|
||||
|
||||
public static boolean isRotationSupported(String mimeType) {
|
||||
if (mimeType == null) return false;
|
||||
mimeType = mimeType.toLowerCase();
|
||||
return mimeType.equals("image/jpeg");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.common;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
|
||||
public class Utils {
|
||||
private static final String TAG = "Utils";
|
||||
private static final String DEBUG_TAG = "GalleryDebug";
|
||||
|
||||
private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
|
||||
private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
|
||||
|
||||
private static long[] sCrcTable = new long[256];
|
||||
|
||||
private static final boolean IS_DEBUG_BUILD =
|
||||
Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug");
|
||||
|
||||
private static final String MASK_STRING = "********************************";
|
||||
|
||||
// Throws AssertionError if the input is false.
|
||||
public static void assertTrue(boolean cond) {
|
||||
if (!cond) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
// Throws AssertionError with the message. We had a method having the form
|
||||
// assertTrue(boolean cond, String message, Object ... args);
|
||||
// However a call to that method will cause memory allocation even if the
|
||||
// condition is false (due to autoboxing generated by "Object ... args"),
|
||||
// so we don't use that anymore.
|
||||
public static void fail(String message, Object ... args) {
|
||||
throw new AssertionError(
|
||||
args.length == 0 ? message : String.format(message, args));
|
||||
}
|
||||
|
||||
// Throws NullPointerException if the input is null.
|
||||
public static <T> T checkNotNull(T object) {
|
||||
if (object == null) throw new NullPointerException();
|
||||
return object;
|
||||
}
|
||||
|
||||
// Returns true if two input Object are both null or equal
|
||||
// to each other.
|
||||
public static boolean equals(Object a, Object b) {
|
||||
return (a == b) || (a == null ? false : a.equals(b));
|
||||
}
|
||||
|
||||
// Returns the next power of two.
|
||||
// Returns the input if it is already power of 2.
|
||||
// Throws IllegalArgumentException if the input is <= 0 or
|
||||
// the answer overflows.
|
||||
public static int nextPowerOf2(int n) {
|
||||
if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n);
|
||||
n -= 1;
|
||||
n |= n >> 16;
|
||||
n |= n >> 8;
|
||||
n |= n >> 4;
|
||||
n |= n >> 2;
|
||||
n |= n >> 1;
|
||||
return n + 1;
|
||||
}
|
||||
|
||||
// Returns the previous power of two.
|
||||
// Returns the input if it is already power of 2.
|
||||
// Throws IllegalArgumentException if the input is <= 0
|
||||
public static int prevPowerOf2(int n) {
|
||||
if (n <= 0) throw new IllegalArgumentException();
|
||||
return Integer.highestOneBit(n);
|
||||
}
|
||||
|
||||
// Returns the input value x clamped to the range [min, max].
|
||||
public static int clamp(int x, int min, int max) {
|
||||
if (x > max) return max;
|
||||
if (x < min) return min;
|
||||
return x;
|
||||
}
|
||||
|
||||
// Returns the input value x clamped to the range [min, max].
|
||||
public static float clamp(float x, float min, float max) {
|
||||
if (x > max) return max;
|
||||
if (x < min) return min;
|
||||
return x;
|
||||
}
|
||||
|
||||
// Returns the input value x clamped to the range [min, max].
|
||||
public static long clamp(long x, long min, long max) {
|
||||
if (x > max) return max;
|
||||
if (x < min) return min;
|
||||
return x;
|
||||
}
|
||||
|
||||
public static boolean isOpaque(int color) {
|
||||
return color >>> 24 == 0xFF;
|
||||
}
|
||||
|
||||
public static void swap(int[] array, int i, int j) {
|
||||
int temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function thats returns a 64-bit crc for string
|
||||
*
|
||||
* @param in input string
|
||||
* @return a 64-bit crc value
|
||||
*/
|
||||
public static final long crc64Long(String in) {
|
||||
if (in == null || in.length() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return crc64Long(getBytes(in));
|
||||
}
|
||||
|
||||
static {
|
||||
// http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
|
||||
long part;
|
||||
for (int i = 0; i < 256; i++) {
|
||||
part = i;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
long x = ((int) part & 1) != 0 ? POLY64REV : 0;
|
||||
part = (part >> 1) ^ x;
|
||||
}
|
||||
sCrcTable[i] = part;
|
||||
}
|
||||
}
|
||||
|
||||
public static final long crc64Long(byte[] buffer) {
|
||||
long crc = INITIALCRC;
|
||||
for (int k = 0, n = buffer.length; k < n; ++k) {
|
||||
crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
public static byte[] getBytes(String in) {
|
||||
byte[] result = new byte[in.length() * 2];
|
||||
int output = 0;
|
||||
for (char ch : in.toCharArray()) {
|
||||
result[output++] = (byte) (ch & 0xFF);
|
||||
result[output++] = (byte) (ch >> 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void closeSilently(Closeable c) {
|
||||
if (c == null) return;
|
||||
try {
|
||||
c.close();
|
||||
} catch (IOException t) {
|
||||
Log.w(TAG, "close fail ", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static int compare(long a, long b) {
|
||||
return a < b ? -1 : a == b ? 0 : 1;
|
||||
}
|
||||
|
||||
public static int ceilLog2(float value) {
|
||||
int i;
|
||||
for (i = 0; i < 31; i++) {
|
||||
if ((1 << i) >= value) break;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public static int floorLog2(float value) {
|
||||
int i;
|
||||
for (i = 0; i < 31; i++) {
|
||||
if ((1 << i) > value) break;
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
public static void closeSilently(ParcelFileDescriptor fd) {
|
||||
try {
|
||||
if (fd != null) fd.close();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "fail to close", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeSilently(Cursor cursor) {
|
||||
try {
|
||||
if (cursor != null) cursor.close();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "fail to close", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static float interpolateAngle(
|
||||
float source, float target, float progress) {
|
||||
// interpolate the angle from source to target
|
||||
// We make the difference in the range of [-179, 180], this is the
|
||||
// shortest path to change source to target.
|
||||
float diff = target - source;
|
||||
if (diff < 0) diff += 360f;
|
||||
if (diff > 180) diff -= 360f;
|
||||
|
||||
float result = source + diff * progress;
|
||||
return result < 0 ? result + 360f : result;
|
||||
}
|
||||
|
||||
public static float interpolateScale(
|
||||
float source, float target, float progress) {
|
||||
return source + progress * (target - source);
|
||||
}
|
||||
|
||||
public static String ensureNotNull(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
public static float parseFloatSafely(String content, float defaultValue) {
|
||||
if (content == null) return defaultValue;
|
||||
try {
|
||||
return Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseIntSafely(String content, int defaultValue) {
|
||||
if (content == null) return defaultValue;
|
||||
try {
|
||||
return Integer.parseInt(content);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isNullOrEmpty(String exifMake) {
|
||||
return TextUtils.isEmpty(exifMake);
|
||||
}
|
||||
|
||||
public static void waitWithoutInterrupt(Object object) {
|
||||
try {
|
||||
object.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "unexpected interrupt: " + object);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean handleInterrruptedException(Throwable e) {
|
||||
// A helper to deal with the interrupt exception
|
||||
// If an interrupt detected, we will setup the bit again.
|
||||
if (e instanceof InterruptedIOException
|
||||
|| e instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String with special XML characters escaped.
|
||||
*/
|
||||
public static String escapeXml(String s) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0, len = s.length(); i < len; ++i) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '<': sb.append("<"); break;
|
||||
case '>': sb.append(">"); break;
|
||||
case '\"': sb.append("""); break;
|
||||
case '\'': sb.append("'"); break;
|
||||
case '&': sb.append("&"); break;
|
||||
default: sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String getUserAgent(Context context) {
|
||||
PackageInfo packageInfo;
|
||||
try {
|
||||
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
} catch (NameNotFoundException e) {
|
||||
throw new IllegalStateException("getPackageInfo failed");
|
||||
}
|
||||
return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
|
||||
packageInfo.packageName,
|
||||
packageInfo.versionName,
|
||||
Build.BRAND,
|
||||
Build.DEVICE,
|
||||
Build.MODEL,
|
||||
Build.ID,
|
||||
Build.VERSION.SDK_INT,
|
||||
Build.VERSION.RELEASE,
|
||||
Build.VERSION.INCREMENTAL);
|
||||
}
|
||||
|
||||
public static String[] copyOf(String[] source, int newSize) {
|
||||
String[] result = new String[newSize];
|
||||
newSize = Math.min(source.length, newSize);
|
||||
System.arraycopy(source, 0, result, 0, newSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mask information for debugging only. It returns <code>info.toString()</code> directly
|
||||
// for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****")
|
||||
// in release build to protect the information (e.g. for privacy issue).
|
||||
public static String maskDebugInfo(Object info) {
|
||||
if (info == null) return null;
|
||||
String s = info.toString();
|
||||
int length = Math.min(s.length(), MASK_STRING.length());
|
||||
return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length);
|
||||
}
|
||||
|
||||
// This method should be ONLY used for debugging.
|
||||
public static void debug(String message, Object ... args) {
|
||||
Log.v(DEBUG_TAG, String.format(message, args));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
class ByteBufferInputStream extends InputStream {
|
||||
|
||||
private ByteBuffer mBuf;
|
||||
|
||||
public ByteBufferInputStream(ByteBuffer buf) {
|
||||
mBuf = buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
if (!mBuf.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
return mBuf.get() & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] bytes, int off, int len) {
|
||||
if (!mBuf.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = Math.min(len, mBuf.remaining());
|
||||
mBuf.get(bytes, off, len);
|
||||
return len;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
class CountedDataInputStream extends FilterInputStream {
|
||||
|
||||
private int mCount = 0;
|
||||
|
||||
// allocate a byte buffer for a long value;
|
||||
private final byte mByteArray[] = new byte[8];
|
||||
private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
|
||||
|
||||
protected CountedDataInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public int getReadByteCount() {
|
||||
return mCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
int r = in.read(b);
|
||||
mCount += (r >= 0) ? r : 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int r = in.read(b, off, len);
|
||||
mCount += (r >= 0) ? r : 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int r = in.read();
|
||||
mCount += (r >= 0) ? 1 : 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long length) throws IOException {
|
||||
long skip = in.skip(length);
|
||||
mCount += skip;
|
||||
return skip;
|
||||
}
|
||||
|
||||
public void skipOrThrow(long length) throws IOException {
|
||||
if (skip(length) != length) throw new EOFException();
|
||||
}
|
||||
|
||||
public void skipTo(long target) throws IOException {
|
||||
long cur = mCount;
|
||||
long diff = target - cur;
|
||||
assert(diff >= 0);
|
||||
skipOrThrow(diff);
|
||||
}
|
||||
|
||||
public void readOrThrow(byte[] b, int off, int len) throws IOException {
|
||||
int r = read(b, off, len);
|
||||
if (r != len) throw new EOFException();
|
||||
}
|
||||
|
||||
public void readOrThrow(byte[] b) throws IOException {
|
||||
readOrThrow(b, 0, b.length);
|
||||
}
|
||||
|
||||
public void setByteOrder(ByteOrder order) {
|
||||
mByteBuffer.order(order);
|
||||
}
|
||||
|
||||
public ByteOrder getByteOrder() {
|
||||
return mByteBuffer.order();
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
readOrThrow(mByteArray, 0 ,2);
|
||||
mByteBuffer.rewind();
|
||||
return mByteBuffer.getShort();
|
||||
}
|
||||
|
||||
public int readUnsignedShort() throws IOException {
|
||||
return readShort() & 0xffff;
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
readOrThrow(mByteArray, 0 , 4);
|
||||
mByteBuffer.rewind();
|
||||
return mByteBuffer.getInt();
|
||||
}
|
||||
|
||||
public long readUnsignedInt() throws IOException {
|
||||
return readInt() & 0xffffffffL;
|
||||
}
|
||||
|
||||
public long readLong() throws IOException {
|
||||
readOrThrow(mByteArray, 0 , 8);
|
||||
mByteBuffer.rewind();
|
||||
return mByteBuffer.getLong();
|
||||
}
|
||||
|
||||
public String readString(int n) throws IOException {
|
||||
byte buf[] = new byte[n];
|
||||
readOrThrow(buf);
|
||||
return new String(buf, "UTF8");
|
||||
}
|
||||
|
||||
public String readString(int n, Charset charset) throws IOException {
|
||||
byte buf[] = new byte[n];
|
||||
readOrThrow(buf);
|
||||
return new String(buf, charset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class stores the EXIF header in IFDs according to the JPEG
|
||||
* specification. It is the result produced by {@link ExifReader}.
|
||||
*
|
||||
* @see ExifReader
|
||||
* @see IfdData
|
||||
*/
|
||||
class ExifData {
|
||||
private static final String TAG = "ExifData";
|
||||
private static final byte[] USER_COMMENT_ASCII = {
|
||||
0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
|
||||
};
|
||||
private static final byte[] USER_COMMENT_JIS = {
|
||||
0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
private static final byte[] USER_COMMENT_UNICODE = {
|
||||
0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
|
||||
};
|
||||
|
||||
private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
|
||||
private byte[] mThumbnail;
|
||||
private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
|
||||
private final ByteOrder mByteOrder;
|
||||
|
||||
ExifData(ByteOrder order) {
|
||||
mByteOrder = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compressed thumbnail. Returns null if there is no compressed
|
||||
* thumbnail.
|
||||
*
|
||||
* @see #hasCompressedThumbnail()
|
||||
*/
|
||||
protected byte[] getCompressedThumbnail() {
|
||||
return mThumbnail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the compressed thumbnail.
|
||||
*/
|
||||
protected void setCompressedThumbnail(byte[] thumbnail) {
|
||||
mThumbnail = thumbnail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true it this header contains a compressed thumbnail.
|
||||
*/
|
||||
protected boolean hasCompressedThumbnail() {
|
||||
return mThumbnail != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an uncompressed strip.
|
||||
*/
|
||||
protected void setStripBytes(int index, byte[] strip) {
|
||||
if (index < mStripBytes.size()) {
|
||||
mStripBytes.set(index, strip);
|
||||
} else {
|
||||
for (int i = mStripBytes.size(); i < index; i++) {
|
||||
mStripBytes.add(null);
|
||||
}
|
||||
mStripBytes.add(strip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the strip count.
|
||||
*/
|
||||
protected int getStripCount() {
|
||||
return mStripBytes.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the strip at the specified index.
|
||||
*
|
||||
* @exceptions #IndexOutOfBoundException
|
||||
*/
|
||||
protected byte[] getStrip(int index) {
|
||||
return mStripBytes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this header contains uncompressed strip.
|
||||
*/
|
||||
protected boolean hasUncompressedStrip() {
|
||||
return mStripBytes.size() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the byte order.
|
||||
*/
|
||||
protected ByteOrder getByteOrder() {
|
||||
return mByteOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link IfdData} object corresponding to a given IFD if it
|
||||
* exists or null.
|
||||
*/
|
||||
protected IfdData getIfdData(int ifdId) {
|
||||
if (ExifTag.isValidIfd(ifdId)) {
|
||||
return mIfdDatas[ifdId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds IFD data. If IFD data of the same type already exists, it will be
|
||||
* replaced by the new data.
|
||||
*/
|
||||
protected void addIfdData(IfdData data) {
|
||||
mIfdDatas[data.getId()] = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link IfdData} object corresponding to a given IFD or
|
||||
* generates one if none exist.
|
||||
*/
|
||||
protected IfdData getOrCreateIfdData(int ifdId) {
|
||||
IfdData ifdData = mIfdDatas[ifdId];
|
||||
if (ifdData == null) {
|
||||
ifdData = new IfdData(ifdId);
|
||||
mIfdDatas[ifdId] = ifdData;
|
||||
}
|
||||
return ifdData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag with a given TID in the given IFD if the tag exists.
|
||||
* Otherwise returns null.
|
||||
*/
|
||||
protected ExifTag getTag(short tag, int ifd) {
|
||||
IfdData ifdData = mIfdDatas[ifd];
|
||||
return (ifdData == null) ? null : ifdData.getTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
|
||||
* with the same TID or null if none exist.
|
||||
*/
|
||||
protected ExifTag addTag(ExifTag tag) {
|
||||
if (tag != null) {
|
||||
int ifd = tag.getIfd();
|
||||
return addTag(tag, ifd);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
|
||||
* with the same TID or null if none exist.
|
||||
*/
|
||||
protected ExifTag addTag(ExifTag tag, int ifdId) {
|
||||
if (tag != null && ExifTag.isValidIfd(ifdId)) {
|
||||
IfdData ifdData = getOrCreateIfdData(ifdId);
|
||||
return ifdData.setTag(tag);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void clearThumbnailAndStrips() {
|
||||
mThumbnail = null;
|
||||
mStripBytes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the thumbnail and its related tags. IFD1 will be removed.
|
||||
*/
|
||||
protected void removeThumbnailData() {
|
||||
clearThumbnailAndStrips();
|
||||
mIfdDatas[IfdId.TYPE_IFD_1] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the tag with a given TID and IFD.
|
||||
*/
|
||||
protected void removeTag(short tagId, int ifdId) {
|
||||
IfdData ifdData = mIfdDatas[ifdId];
|
||||
if (ifdData == null) {
|
||||
return;
|
||||
}
|
||||
ifdData.removeTag(tagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the user comment tag into string as specified in the EXIF
|
||||
* standard. Returns null if decoding failed.
|
||||
*/
|
||||
protected String getUserComment() {
|
||||
IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
|
||||
if (ifdData == null) {
|
||||
return null;
|
||||
}
|
||||
ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
|
||||
if (tag == null) {
|
||||
return null;
|
||||
}
|
||||
if (tag.getComponentCount() < 8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] buf = new byte[tag.getComponentCount()];
|
||||
tag.getBytes(buf);
|
||||
|
||||
byte[] code = new byte[8];
|
||||
System.arraycopy(buf, 0, code, 0, 8);
|
||||
|
||||
try {
|
||||
if (Arrays.equals(code, USER_COMMENT_ASCII)) {
|
||||
return new String(buf, 8, buf.length - 8, "US-ASCII");
|
||||
} else if (Arrays.equals(code, USER_COMMENT_JIS)) {
|
||||
return new String(buf, 8, buf.length - 8, "EUC-JP");
|
||||
} else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
|
||||
return new String(buf, 8, buf.length - 8, "UTF-16");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.w(TAG, "Failed to decode the user comment");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all {@link ExifTag}s in the ExifData or null if there
|
||||
* are none.
|
||||
*/
|
||||
protected List<ExifTag> getAllTags() {
|
||||
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
|
||||
for (IfdData d : mIfdDatas) {
|
||||
if (d != null) {
|
||||
ExifTag[] tags = d.getAllTags();
|
||||
if (tags != null) {
|
||||
for (ExifTag t : tags) {
|
||||
ret.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ret.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all {@link ExifTag}s in a given IFD or null if there
|
||||
* are none.
|
||||
*/
|
||||
protected List<ExifTag> getAllTagsForIfd(int ifd) {
|
||||
IfdData d = mIfdDatas[ifd];
|
||||
if (d == null) {
|
||||
return null;
|
||||
}
|
||||
ExifTag[] tags = d.getAllTags();
|
||||
if (tags == null) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
|
||||
for (ExifTag t : tags) {
|
||||
ret.add(t);
|
||||
}
|
||||
if (ret.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all {@link ExifTag}s with a given TID or null if there
|
||||
* are none.
|
||||
*/
|
||||
protected List<ExifTag> getAllTagsForTagId(short tag) {
|
||||
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
|
||||
for (IfdData d : mIfdDatas) {
|
||||
if (d != null) {
|
||||
ExifTag t = d.getTag(tag);
|
||||
if (t != null) {
|
||||
ret.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ret.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (obj instanceof ExifData) {
|
||||
ExifData data = (ExifData) obj;
|
||||
if (data.mByteOrder != mByteOrder ||
|
||||
data.mStripBytes.size() != mStripBytes.size() ||
|
||||
!Arrays.equals(data.mThumbnail, mThumbnail)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < mStripBytes.size(); i++) {
|
||||
if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
|
||||
IfdData ifd1 = data.getIfdData(i);
|
||||
IfdData ifd2 = getIfdData(i);
|
||||
if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
public class ExifInvalidFormatException extends Exception {
|
||||
public ExifInvalidFormatException(String meg) {
|
||||
super(meg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class ExifModifier {
|
||||
public static final String TAG = "ExifModifier";
|
||||
public static final boolean DEBUG = false;
|
||||
private final ByteBuffer mByteBuffer;
|
||||
private final ExifData mTagToModified;
|
||||
private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>();
|
||||
private final ExifInterface mInterface;
|
||||
private int mOffsetBase;
|
||||
|
||||
private static class TagOffset {
|
||||
final int mOffset;
|
||||
final ExifTag mTag;
|
||||
|
||||
TagOffset(ExifTag tag, int offset) {
|
||||
mTag = tag;
|
||||
mOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException,
|
||||
ExifInvalidFormatException {
|
||||
mByteBuffer = byteBuffer;
|
||||
mOffsetBase = byteBuffer.position();
|
||||
mInterface = iRef;
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = new ByteBufferInputStream(byteBuffer);
|
||||
// Do not require any IFD;
|
||||
ExifParser parser = ExifParser.parse(is, mInterface);
|
||||
mTagToModified = new ExifData(parser.getByteOrder());
|
||||
mOffsetBase += parser.getTiffStartPosition();
|
||||
mByteBuffer.position(0);
|
||||
} finally {
|
||||
ExifInterface.closeSilently(is);
|
||||
}
|
||||
}
|
||||
|
||||
protected ByteOrder getByteOrder() {
|
||||
return mTagToModified.getByteOrder();
|
||||
}
|
||||
|
||||
protected boolean commit() throws IOException, ExifInvalidFormatException {
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = new ByteBufferInputStream(mByteBuffer);
|
||||
int flag = 0;
|
||||
IfdData[] ifdDatas = new IfdData[] {
|
||||
mTagToModified.getIfdData(IfdId.TYPE_IFD_0),
|
||||
mTagToModified.getIfdData(IfdId.TYPE_IFD_1),
|
||||
mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF),
|
||||
mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY),
|
||||
mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS)
|
||||
};
|
||||
|
||||
if (ifdDatas[IfdId.TYPE_IFD_0] != null) {
|
||||
flag |= ExifParser.OPTION_IFD_0;
|
||||
}
|
||||
if (ifdDatas[IfdId.TYPE_IFD_1] != null) {
|
||||
flag |= ExifParser.OPTION_IFD_1;
|
||||
}
|
||||
if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) {
|
||||
flag |= ExifParser.OPTION_IFD_EXIF;
|
||||
}
|
||||
if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) {
|
||||
flag |= ExifParser.OPTION_IFD_GPS;
|
||||
}
|
||||
if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) {
|
||||
flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
|
||||
}
|
||||
|
||||
ExifParser parser = ExifParser.parse(is, flag, mInterface);
|
||||
int event = parser.next();
|
||||
IfdData currIfd = null;
|
||||
while (event != ExifParser.EVENT_END) {
|
||||
switch (event) {
|
||||
case ExifParser.EVENT_START_OF_IFD:
|
||||
currIfd = ifdDatas[parser.getCurrentIfd()];
|
||||
if (currIfd == null) {
|
||||
parser.skipRemainingTagsInCurrentIfd();
|
||||
}
|
||||
break;
|
||||
case ExifParser.EVENT_NEW_TAG:
|
||||
ExifTag oldTag = parser.getTag();
|
||||
ExifTag newTag = currIfd.getTag(oldTag.getTagId());
|
||||
if (newTag != null) {
|
||||
if (newTag.getComponentCount() != oldTag.getComponentCount()
|
||||
|| newTag.getDataType() != oldTag.getDataType()) {
|
||||
return false;
|
||||
} else {
|
||||
mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset()));
|
||||
currIfd.removeTag(oldTag.getTagId());
|
||||
if (currIfd.getTagCount() == 0) {
|
||||
parser.skipRemainingTagsInCurrentIfd();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
event = parser.next();
|
||||
}
|
||||
for (IfdData ifd : ifdDatas) {
|
||||
if (ifd != null && ifd.getTagCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
modify();
|
||||
} finally {
|
||||
ExifInterface.closeSilently(is);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void modify() {
|
||||
mByteBuffer.order(getByteOrder());
|
||||
for (TagOffset tagOffset : mTagOffsets) {
|
||||
writeTagValue(tagOffset.mTag, tagOffset.mOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTagValue(ExifTag tag, int offset) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "modifying tag to: \n" + tag.toString());
|
||||
Log.v(TAG, "at offset: " + offset);
|
||||
}
|
||||
mByteBuffer.position(offset + mOffsetBase);
|
||||
switch (tag.getDataType()) {
|
||||
case ExifTag.TYPE_ASCII:
|
||||
byte buf[] = tag.getStringByte();
|
||||
if (buf.length == tag.getComponentCount()) {
|
||||
buf[buf.length - 1] = 0;
|
||||
mByteBuffer.put(buf);
|
||||
} else {
|
||||
mByteBuffer.put(buf);
|
||||
mByteBuffer.put((byte) 0);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_LONG:
|
||||
case ExifTag.TYPE_UNSIGNED_LONG:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
mByteBuffer.putInt((int) tag.getValueAt(i));
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_RATIONAL:
|
||||
case ExifTag.TYPE_UNSIGNED_RATIONAL:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
Rational v = tag.getRational(i);
|
||||
mByteBuffer.putInt((int) v.getNumerator());
|
||||
mByteBuffer.putInt((int) v.getDenominator());
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_UNDEFINED:
|
||||
case ExifTag.TYPE_UNSIGNED_BYTE:
|
||||
buf = new byte[tag.getComponentCount()];
|
||||
tag.getBytes(buf);
|
||||
mByteBuffer.put(buf);
|
||||
break;
|
||||
case ExifTag.TYPE_UNSIGNED_SHORT:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
mByteBuffer.putShort((short) tag.getValueAt(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void modifyTag(ExifTag tag) {
|
||||
mTagToModified.addTag(tag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class provides a way to replace the Exif header of a JPEG image.
|
||||
* <p>
|
||||
* Below is an example of writing EXIF data into a file
|
||||
*
|
||||
* <pre>
|
||||
* public static void writeExif(byte[] jpeg, ExifData exif, String path) {
|
||||
* OutputStream os = null;
|
||||
* try {
|
||||
* os = new FileOutputStream(path);
|
||||
* ExifOutputStream eos = new ExifOutputStream(os);
|
||||
* // Set the exif header
|
||||
* eos.setExifData(exif);
|
||||
* // Write the original jpeg out, the header will be add into the file.
|
||||
* eos.write(jpeg);
|
||||
* } catch (FileNotFoundException e) {
|
||||
* e.printStackTrace();
|
||||
* } catch (IOException e) {
|
||||
* e.printStackTrace();
|
||||
* } finally {
|
||||
* if (os != null) {
|
||||
* try {
|
||||
* os.close();
|
||||
* } catch (IOException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
class ExifOutputStream extends FilterOutputStream {
|
||||
private static final String TAG = "ExifOutputStream";
|
||||
private static final boolean DEBUG = false;
|
||||
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
|
||||
|
||||
private static final int STATE_SOI = 0;
|
||||
private static final int STATE_FRAME_HEADER = 1;
|
||||
private static final int STATE_JPEG_DATA = 2;
|
||||
|
||||
private static final int EXIF_HEADER = 0x45786966;
|
||||
private static final short TIFF_HEADER = 0x002A;
|
||||
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
|
||||
private static final short TIFF_LITTLE_ENDIAN = 0x4949;
|
||||
private static final short TAG_SIZE = 12;
|
||||
private static final short TIFF_HEADER_SIZE = 8;
|
||||
private static final int MAX_EXIF_SIZE = 65535;
|
||||
|
||||
private ExifData mExifData;
|
||||
private int mState = STATE_SOI;
|
||||
private int mByteToSkip;
|
||||
private int mByteToCopy;
|
||||
private byte[] mSingleByteArray = new byte[1];
|
||||
private ByteBuffer mBuffer = ByteBuffer.allocate(4);
|
||||
private final ExifInterface mInterface;
|
||||
|
||||
protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
|
||||
super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
|
||||
mInterface = iRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ExifData to be written into the JPEG file. Should be called
|
||||
* before writing image data.
|
||||
*/
|
||||
protected void setExifData(ExifData exifData) {
|
||||
mExifData = exifData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Exif header to be written into the JPEF file.
|
||||
*/
|
||||
protected ExifData getExifData() {
|
||||
return mExifData;
|
||||
}
|
||||
|
||||
private int requestByteToBuffer(int requestByteCount, byte[] buffer
|
||||
, int offset, int length) {
|
||||
int byteNeeded = requestByteCount - mBuffer.position();
|
||||
int byteToRead = length > byteNeeded ? byteNeeded : length;
|
||||
mBuffer.put(buffer, offset, byteToRead);
|
||||
return byteToRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the image out. The input data should be a valid JPEG format. After
|
||||
* writing, it's Exif header will be replaced by the given header.
|
||||
*/
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||
while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
|
||||
&& length > 0) {
|
||||
if (mByteToSkip > 0) {
|
||||
int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
|
||||
length -= byteToProcess;
|
||||
mByteToSkip -= byteToProcess;
|
||||
offset += byteToProcess;
|
||||
}
|
||||
if (mByteToCopy > 0) {
|
||||
int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
|
||||
out.write(buffer, offset, byteToProcess);
|
||||
length -= byteToProcess;
|
||||
mByteToCopy -= byteToProcess;
|
||||
offset += byteToProcess;
|
||||
}
|
||||
if (length == 0) {
|
||||
return;
|
||||
}
|
||||
switch (mState) {
|
||||
case STATE_SOI:
|
||||
int byteRead = requestByteToBuffer(2, buffer, offset, length);
|
||||
offset += byteRead;
|
||||
length -= byteRead;
|
||||
if (mBuffer.position() < 2) {
|
||||
return;
|
||||
}
|
||||
mBuffer.rewind();
|
||||
if (mBuffer.getShort() != JpegHeader.SOI) {
|
||||
throw new IOException("Not a valid jpeg image, cannot write exif");
|
||||
}
|
||||
out.write(mBuffer.array(), 0, 2);
|
||||
mState = STATE_FRAME_HEADER;
|
||||
mBuffer.rewind();
|
||||
writeExifData();
|
||||
break;
|
||||
case STATE_FRAME_HEADER:
|
||||
// We ignore the APP1 segment and copy all other segments
|
||||
// until SOF tag.
|
||||
byteRead = requestByteToBuffer(4, buffer, offset, length);
|
||||
offset += byteRead;
|
||||
length -= byteRead;
|
||||
// Check if this image data doesn't contain SOF.
|
||||
if (mBuffer.position() == 2) {
|
||||
short tag = mBuffer.getShort();
|
||||
if (tag == JpegHeader.EOI) {
|
||||
out.write(mBuffer.array(), 0, 2);
|
||||
mBuffer.rewind();
|
||||
}
|
||||
}
|
||||
if (mBuffer.position() < 4) {
|
||||
return;
|
||||
}
|
||||
mBuffer.rewind();
|
||||
short marker = mBuffer.getShort();
|
||||
if (marker == JpegHeader.APP1) {
|
||||
mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
|
||||
mState = STATE_JPEG_DATA;
|
||||
} else if (!JpegHeader.isSofMarker(marker)) {
|
||||
out.write(mBuffer.array(), 0, 4);
|
||||
mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
|
||||
} else {
|
||||
out.write(mBuffer.array(), 0, 4);
|
||||
mState = STATE_JPEG_DATA;
|
||||
}
|
||||
mBuffer.rewind();
|
||||
}
|
||||
}
|
||||
if (length > 0) {
|
||||
out.write(buffer, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the one bytes out. The input data should be a valid JPEG format.
|
||||
* After writing, it's Exif header will be replaced by the given header.
|
||||
*/
|
||||
@Override
|
||||
public void write(int oneByte) throws IOException {
|
||||
mSingleByteArray[0] = (byte) (0xff & oneByte);
|
||||
write(mSingleByteArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to calling write(buffer, 0, buffer.length).
|
||||
*/
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
write(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
private void writeExifData() throws IOException {
|
||||
if (mExifData == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Writing exif data...");
|
||||
}
|
||||
ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
|
||||
createRequiredIfdAndTag();
|
||||
int exifSize = calculateAllOffset();
|
||||
if (exifSize + 8 > MAX_EXIF_SIZE) {
|
||||
throw new IOException("Exif header is too large (>64Kb)");
|
||||
}
|
||||
OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
|
||||
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
|
||||
dataOutputStream.writeShort(JpegHeader.APP1);
|
||||
dataOutputStream.writeShort((short) (exifSize + 8));
|
||||
dataOutputStream.writeInt(EXIF_HEADER);
|
||||
dataOutputStream.writeShort((short) 0x0000);
|
||||
if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
|
||||
dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
|
||||
} else {
|
||||
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
|
||||
}
|
||||
dataOutputStream.setByteOrder(mExifData.getByteOrder());
|
||||
dataOutputStream.writeShort(TIFF_HEADER);
|
||||
dataOutputStream.writeInt(8);
|
||||
writeAllTags(dataOutputStream);
|
||||
writeThumbnail(dataOutputStream);
|
||||
for (ExifTag t : nullTags) {
|
||||
mExifData.addTag(t);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
|
||||
ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
|
||||
for(ExifTag t : data.getAllTags()) {
|
||||
if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
|
||||
data.removeTag(t.getTagId(), t.getIfd());
|
||||
nullTags.add(t);
|
||||
}
|
||||
}
|
||||
return nullTags;
|
||||
}
|
||||
|
||||
private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
|
||||
if (mExifData.hasCompressedThumbnail()) {
|
||||
dataOutputStream.write(mExifData.getCompressedThumbnail());
|
||||
} else if (mExifData.hasUncompressedStrip()) {
|
||||
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
||||
dataOutputStream.write(mExifData.getStrip(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
|
||||
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
|
||||
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
|
||||
IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
||||
if (interoperabilityIfd != null) {
|
||||
writeIfd(interoperabilityIfd, dataOutputStream);
|
||||
}
|
||||
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
||||
if (gpsIfd != null) {
|
||||
writeIfd(gpsIfd, dataOutputStream);
|
||||
}
|
||||
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
||||
if (ifd1 != null) {
|
||||
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
|
||||
throws IOException {
|
||||
ExifTag[] tags = ifd.getAllTags();
|
||||
dataOutputStream.writeShort((short) tags.length);
|
||||
for (ExifTag tag : tags) {
|
||||
dataOutputStream.writeShort(tag.getTagId());
|
||||
dataOutputStream.writeShort(tag.getDataType());
|
||||
dataOutputStream.writeInt(tag.getComponentCount());
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "\n" + tag.toString());
|
||||
}
|
||||
if (tag.getDataSize() > 4) {
|
||||
dataOutputStream.writeInt(tag.getOffset());
|
||||
} else {
|
||||
ExifOutputStream.writeTagValue(tag, dataOutputStream);
|
||||
for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
|
||||
dataOutputStream.write(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
|
||||
for (ExifTag tag : tags) {
|
||||
if (tag.getDataSize() > 4) {
|
||||
ExifOutputStream.writeTagValue(tag, dataOutputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateOffsetOfIfd(IfdData ifd, int offset) {
|
||||
offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
|
||||
ExifTag[] tags = ifd.getAllTags();
|
||||
for (ExifTag tag : tags) {
|
||||
if (tag.getDataSize() > 4) {
|
||||
tag.setOffset(offset);
|
||||
offset += tag.getDataSize();
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
private void createRequiredIfdAndTag() throws IOException {
|
||||
// IFD0 is required for all file
|
||||
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
|
||||
if (ifd0 == null) {
|
||||
ifd0 = new IfdData(IfdId.TYPE_IFD_0);
|
||||
mExifData.addIfdData(ifd0);
|
||||
}
|
||||
ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
|
||||
if (exifOffsetTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_EXIF_IFD);
|
||||
}
|
||||
ifd0.setTag(exifOffsetTag);
|
||||
|
||||
// Exif IFD is required for all files.
|
||||
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
|
||||
if (exifIfd == null) {
|
||||
exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
|
||||
mExifData.addIfdData(exifIfd);
|
||||
}
|
||||
|
||||
// GPS IFD
|
||||
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
||||
if (gpsIfd != null) {
|
||||
ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
|
||||
if (gpsOffsetTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_GPS_IFD);
|
||||
}
|
||||
ifd0.setTag(gpsOffsetTag);
|
||||
}
|
||||
|
||||
// Interoperability IFD
|
||||
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
||||
if (interIfd != null) {
|
||||
ExifTag interOffsetTag = mInterface
|
||||
.buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
|
||||
if (interOffsetTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_INTEROPERABILITY_IFD);
|
||||
}
|
||||
exifIfd.setTag(interOffsetTag);
|
||||
}
|
||||
|
||||
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
||||
|
||||
// thumbnail
|
||||
if (mExifData.hasCompressedThumbnail()) {
|
||||
|
||||
if (ifd1 == null) {
|
||||
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
|
||||
mExifData.addIfdData(ifd1);
|
||||
}
|
||||
|
||||
ExifTag offsetTag = mInterface
|
||||
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
|
||||
if (offsetTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
|
||||
}
|
||||
|
||||
ifd1.setTag(offsetTag);
|
||||
ExifTag lengthTag = mInterface
|
||||
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
if (lengthTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
}
|
||||
|
||||
lengthTag.setValue(mExifData.getCompressedThumbnail().length);
|
||||
ifd1.setTag(lengthTag);
|
||||
|
||||
// Get rid of tags for uncompressed if they exist.
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
|
||||
} else if (mExifData.hasUncompressedStrip()) {
|
||||
if (ifd1 == null) {
|
||||
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
|
||||
mExifData.addIfdData(ifd1);
|
||||
}
|
||||
int stripCount = mExifData.getStripCount();
|
||||
ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
|
||||
if (offsetTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_STRIP_OFFSETS);
|
||||
}
|
||||
ExifTag lengthTag = mInterface
|
||||
.buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
|
||||
if (lengthTag == null) {
|
||||
throw new IOException("No definition for crucial exif tag: "
|
||||
+ ExifInterface.TAG_STRIP_BYTE_COUNTS);
|
||||
}
|
||||
long[] lengths = new long[stripCount];
|
||||
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
||||
lengths[i] = mExifData.getStrip(i).length;
|
||||
}
|
||||
lengthTag.setValue(lengths);
|
||||
ifd1.setTag(offsetTag);
|
||||
ifd1.setTag(lengthTag);
|
||||
// Get rid of tags for compressed if they exist.
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
|
||||
ifd1.removeTag(ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
|
||||
} else if (ifd1 != null) {
|
||||
// Get rid of offset and length tags if there is no thumbnail.
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
|
||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
|
||||
ifd1.removeTag(ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateAllOffset() {
|
||||
int offset = TIFF_HEADER_SIZE;
|
||||
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
|
||||
offset = calculateOffsetOfIfd(ifd0, offset);
|
||||
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
|
||||
|
||||
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
|
||||
offset = calculateOffsetOfIfd(exifIfd, offset);
|
||||
|
||||
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
||||
if (interIfd != null) {
|
||||
exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
|
||||
.setValue(offset);
|
||||
offset = calculateOffsetOfIfd(interIfd, offset);
|
||||
}
|
||||
|
||||
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
||||
if (gpsIfd != null) {
|
||||
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
|
||||
offset = calculateOffsetOfIfd(gpsIfd, offset);
|
||||
}
|
||||
|
||||
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
||||
if (ifd1 != null) {
|
||||
ifd0.setOffsetToNextIfd(offset);
|
||||
offset = calculateOffsetOfIfd(ifd1, offset);
|
||||
}
|
||||
|
||||
// thumbnail
|
||||
if (mExifData.hasCompressedThumbnail()) {
|
||||
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
|
||||
.setValue(offset);
|
||||
offset += mExifData.getCompressedThumbnail().length;
|
||||
} else if (mExifData.hasUncompressedStrip()) {
|
||||
int stripCount = mExifData.getStripCount();
|
||||
long[] offsets = new long[stripCount];
|
||||
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
||||
offsets[i] = offset;
|
||||
offset += mExifData.getStrip(i).length;
|
||||
}
|
||||
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
|
||||
offsets);
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
|
||||
throws IOException {
|
||||
switch (tag.getDataType()) {
|
||||
case ExifTag.TYPE_ASCII:
|
||||
byte buf[] = tag.getStringByte();
|
||||
if (buf.length == tag.getComponentCount()) {
|
||||
buf[buf.length - 1] = 0;
|
||||
dataOutputStream.write(buf);
|
||||
} else {
|
||||
dataOutputStream.write(buf);
|
||||
dataOutputStream.write(0);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_LONG:
|
||||
case ExifTag.TYPE_UNSIGNED_LONG:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
dataOutputStream.writeInt((int) tag.getValueAt(i));
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_RATIONAL:
|
||||
case ExifTag.TYPE_UNSIGNED_RATIONAL:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
dataOutputStream.writeRational(tag.getRational(i));
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_UNDEFINED:
|
||||
case ExifTag.TYPE_UNSIGNED_BYTE:
|
||||
buf = new byte[tag.getComponentCount()];
|
||||
tag.getBytes(buf);
|
||||
dataOutputStream.write(buf);
|
||||
break;
|
||||
case ExifTag.TYPE_UNSIGNED_SHORT:
|
||||
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
||||
dataOutputStream.writeShort((short) tag.getValueAt(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,916 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* This class provides a low-level EXIF parsing API. Given a JPEG format
|
||||
* InputStream, the caller can request which IFD's to read via
|
||||
* {@link #parse(InputStream, int)} with given options.
|
||||
* <p>
|
||||
* Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the
|
||||
* parser.
|
||||
*
|
||||
* <pre>
|
||||
* void parse() {
|
||||
* ExifParser parser = ExifParser.parse(mImageInputStream,
|
||||
* ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
|
||||
* int event = parser.next();
|
||||
* while (event != ExifParser.EVENT_END) {
|
||||
* switch (event) {
|
||||
* case ExifParser.EVENT_START_OF_IFD:
|
||||
* break;
|
||||
* case ExifParser.EVENT_NEW_TAG:
|
||||
* ExifTag tag = parser.getTag();
|
||||
* if (!tag.hasValue()) {
|
||||
* parser.registerForTagValue(tag);
|
||||
* } else {
|
||||
* processTag(tag);
|
||||
* }
|
||||
* break;
|
||||
* case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
|
||||
* tag = parser.getTag();
|
||||
* if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
|
||||
* processTag(tag);
|
||||
* }
|
||||
* break;
|
||||
* }
|
||||
* event = parser.next();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* void processTag(ExifTag tag) {
|
||||
* // process the tag as you like.
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
class ExifParser {
|
||||
private static final boolean LOGV = false;
|
||||
private static final String TAG = "ExifParser";
|
||||
/**
|
||||
* When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to
|
||||
* know which IFD we are in.
|
||||
*/
|
||||
public static final int EVENT_START_OF_IFD = 0;
|
||||
/**
|
||||
* When the parser reaches a new tag. Call {@link #getTag()}to get the
|
||||
* corresponding tag.
|
||||
*/
|
||||
public static final int EVENT_NEW_TAG = 1;
|
||||
/**
|
||||
* When the parser reaches the value area of tag that is registered by
|
||||
* {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()}
|
||||
* to get the corresponding tag.
|
||||
*/
|
||||
public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
|
||||
|
||||
/**
|
||||
* When the parser reaches the compressed image area.
|
||||
*/
|
||||
public static final int EVENT_COMPRESSED_IMAGE = 3;
|
||||
/**
|
||||
* When the parser reaches the uncompressed image strip. Call
|
||||
* {@link #getStripIndex()} to get the index of the strip.
|
||||
*
|
||||
* @see #getStripIndex()
|
||||
* @see #getStripCount()
|
||||
*/
|
||||
public static final int EVENT_UNCOMPRESSED_STRIP = 4;
|
||||
/**
|
||||
* When there is nothing more to parse.
|
||||
*/
|
||||
public static final int EVENT_END = 5;
|
||||
|
||||
/**
|
||||
* Option bit to request to parse IFD0.
|
||||
*/
|
||||
public static final int OPTION_IFD_0 = 1 << 0;
|
||||
/**
|
||||
* Option bit to request to parse IFD1.
|
||||
*/
|
||||
public static final int OPTION_IFD_1 = 1 << 1;
|
||||
/**
|
||||
* Option bit to request to parse Exif-IFD.
|
||||
*/
|
||||
public static final int OPTION_IFD_EXIF = 1 << 2;
|
||||
/**
|
||||
* Option bit to request to parse GPS-IFD.
|
||||
*/
|
||||
public static final int OPTION_IFD_GPS = 1 << 3;
|
||||
/**
|
||||
* Option bit to request to parse Interoperability-IFD.
|
||||
*/
|
||||
public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
|
||||
/**
|
||||
* Option bit to request to parse thumbnail.
|
||||
*/
|
||||
public static final int OPTION_THUMBNAIL = 1 << 5;
|
||||
|
||||
protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
|
||||
protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
|
||||
|
||||
// TIFF header
|
||||
protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
|
||||
protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
|
||||
protected static final short TIFF_HEADER_TAIL = 0x002A;
|
||||
|
||||
protected static final int TAG_SIZE = 12;
|
||||
protected static final int OFFSET_SIZE = 2;
|
||||
|
||||
private static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||
|
||||
protected static final int DEFAULT_IFD0_OFFSET = 8;
|
||||
|
||||
private final CountedDataInputStream mTiffStream;
|
||||
private final int mOptions;
|
||||
private int mIfdStartOffset = 0;
|
||||
private int mNumOfTagInIfd = 0;
|
||||
private int mIfdType;
|
||||
private ExifTag mTag;
|
||||
private ImageEvent mImageEvent;
|
||||
private int mStripCount;
|
||||
private ExifTag mStripSizeTag;
|
||||
private ExifTag mJpegSizeTag;
|
||||
private boolean mNeedToParseOffsetsInCurrentIfd;
|
||||
private boolean mContainExifData = false;
|
||||
private int mApp1End;
|
||||
private int mOffsetToApp1EndFromSOF = 0;
|
||||
private byte[] mDataAboveIfd0;
|
||||
private int mIfd0Position;
|
||||
private int mTiffStartPosition;
|
||||
private final ExifInterface mInterface;
|
||||
|
||||
private static final short TAG_EXIF_IFD = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
|
||||
private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
|
||||
private static final short TAG_INTEROPERABILITY_IFD = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
|
||||
private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
|
||||
private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
private static final short TAG_STRIP_OFFSETS = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
|
||||
private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface
|
||||
.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
|
||||
|
||||
private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
|
||||
|
||||
private boolean isIfdRequested(int ifdType) {
|
||||
switch (ifdType) {
|
||||
case IfdId.TYPE_IFD_0:
|
||||
return (mOptions & OPTION_IFD_0) != 0;
|
||||
case IfdId.TYPE_IFD_1:
|
||||
return (mOptions & OPTION_IFD_1) != 0;
|
||||
case IfdId.TYPE_IFD_EXIF:
|
||||
return (mOptions & OPTION_IFD_EXIF) != 0;
|
||||
case IfdId.TYPE_IFD_GPS:
|
||||
return (mOptions & OPTION_IFD_GPS) != 0;
|
||||
case IfdId.TYPE_IFD_INTEROPERABILITY:
|
||||
return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isThumbnailRequested() {
|
||||
return (mOptions & OPTION_THUMBNAIL) != 0;
|
||||
}
|
||||
|
||||
private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
|
||||
throws IOException, ExifInvalidFormatException {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Null argument inputStream to ExifParser");
|
||||
}
|
||||
if (LOGV) {
|
||||
Log.v(TAG, "Reading exif...");
|
||||
}
|
||||
mInterface = iRef;
|
||||
mContainExifData = seekTiffData(inputStream);
|
||||
mTiffStream = new CountedDataInputStream(inputStream);
|
||||
mOptions = options;
|
||||
if (!mContainExifData) {
|
||||
return;
|
||||
}
|
||||
|
||||
parseTiffHeader();
|
||||
long offset = mTiffStream.readUnsignedInt();
|
||||
if (offset > Integer.MAX_VALUE) {
|
||||
throw new ExifInvalidFormatException("Invalid offset " + offset);
|
||||
}
|
||||
mIfd0Position = (int) offset;
|
||||
mIfdType = IfdId.TYPE_IFD_0;
|
||||
if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
|
||||
registerIfd(IfdId.TYPE_IFD_0, offset);
|
||||
if (offset != DEFAULT_IFD0_OFFSET) {
|
||||
mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
|
||||
read(mDataAboveIfd0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the the given InputStream with the given options
|
||||
*
|
||||
* @exception IOException
|
||||
* @exception ExifInvalidFormatException
|
||||
*/
|
||||
protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
|
||||
throws IOException, ExifInvalidFormatException {
|
||||
return new ExifParser(inputStream, options, iRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the the given InputStream with default options; that is, every IFD
|
||||
* and thumbnaill will be parsed.
|
||||
*
|
||||
* @exception IOException
|
||||
* @exception ExifInvalidFormatException
|
||||
* @see #parse(InputStream, int)
|
||||
*/
|
||||
protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
|
||||
throws IOException, ExifInvalidFormatException {
|
||||
return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1
|
||||
| OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY
|
||||
| OPTION_THUMBNAIL, iRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the parser forward and returns the next parsing event
|
||||
*
|
||||
* @exception IOException
|
||||
* @exception ExifInvalidFormatException
|
||||
* @see #EVENT_START_OF_IFD
|
||||
* @see #EVENT_NEW_TAG
|
||||
* @see #EVENT_VALUE_OF_REGISTERED_TAG
|
||||
* @see #EVENT_COMPRESSED_IMAGE
|
||||
* @see #EVENT_UNCOMPRESSED_STRIP
|
||||
* @see #EVENT_END
|
||||
*/
|
||||
protected int next() throws IOException, ExifInvalidFormatException {
|
||||
if (!mContainExifData) {
|
||||
return EVENT_END;
|
||||
}
|
||||
int offset = mTiffStream.getReadByteCount();
|
||||
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
|
||||
if (offset < endOfTags) {
|
||||
mTag = readTag();
|
||||
if (mTag == null) {
|
||||
return next();
|
||||
}
|
||||
if (mNeedToParseOffsetsInCurrentIfd) {
|
||||
checkOffsetOrImageTag(mTag);
|
||||
}
|
||||
return EVENT_NEW_TAG;
|
||||
} else if (offset == endOfTags) {
|
||||
// There is a link to ifd1 at the end of ifd0
|
||||
if (mIfdType == IfdId.TYPE_IFD_0) {
|
||||
long ifdOffset = readUnsignedLong();
|
||||
if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
|
||||
if (ifdOffset != 0) {
|
||||
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int offsetSize = 4;
|
||||
// Some camera models use invalid length of the offset
|
||||
if (mCorrespondingEvent.size() > 0) {
|
||||
offsetSize = mCorrespondingEvent.firstEntry().getKey() -
|
||||
mTiffStream.getReadByteCount();
|
||||
}
|
||||
if (offsetSize < 4) {
|
||||
Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize);
|
||||
} else {
|
||||
long ifdOffset = readUnsignedLong();
|
||||
if (ifdOffset != 0) {
|
||||
Log.w(TAG, "Invalid link to next IFD: " + ifdOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (mCorrespondingEvent.size() != 0) {
|
||||
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
|
||||
Object event = entry.getValue();
|
||||
try {
|
||||
skipTo(entry.getKey());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to skip to data at: " + entry.getKey() +
|
||||
" for " + event.getClass().getName() + ", the file may be broken.");
|
||||
continue;
|
||||
}
|
||||
if (event instanceof IfdEvent) {
|
||||
mIfdType = ((IfdEvent) event).ifd;
|
||||
mNumOfTagInIfd = mTiffStream.readUnsignedShort();
|
||||
mIfdStartOffset = entry.getKey();
|
||||
|
||||
if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
|
||||
Log.w(TAG, "Invalid size of IFD " + mIfdType);
|
||||
return EVENT_END;
|
||||
}
|
||||
|
||||
mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
|
||||
if (((IfdEvent) event).isRequested) {
|
||||
return EVENT_START_OF_IFD;
|
||||
} else {
|
||||
skipRemainingTagsInCurrentIfd();
|
||||
}
|
||||
} else if (event instanceof ImageEvent) {
|
||||
mImageEvent = (ImageEvent) event;
|
||||
return mImageEvent.type;
|
||||
} else {
|
||||
ExifTagEvent tagEvent = (ExifTagEvent) event;
|
||||
mTag = tagEvent.tag;
|
||||
if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
|
||||
readFullTagValue(mTag);
|
||||
checkOffsetOrImageTag(mTag);
|
||||
}
|
||||
if (tagEvent.isRequested) {
|
||||
return EVENT_VALUE_OF_REGISTERED_TAG;
|
||||
}
|
||||
}
|
||||
}
|
||||
return EVENT_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the tags area of current IFD, if the parser is not in the tag area,
|
||||
* nothing will happen.
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ExifInvalidFormatException
|
||||
*/
|
||||
protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
|
||||
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
|
||||
int offset = mTiffStream.getReadByteCount();
|
||||
if (offset > endOfTags) {
|
||||
return;
|
||||
}
|
||||
if (mNeedToParseOffsetsInCurrentIfd) {
|
||||
while (offset < endOfTags) {
|
||||
mTag = readTag();
|
||||
offset += TAG_SIZE;
|
||||
if (mTag == null) {
|
||||
continue;
|
||||
}
|
||||
checkOffsetOrImageTag(mTag);
|
||||
}
|
||||
} else {
|
||||
skipTo(endOfTags);
|
||||
}
|
||||
long ifdOffset = readUnsignedLong();
|
||||
// For ifd0, there is a link to ifd1 in the end of all tags
|
||||
if (mIfdType == IfdId.TYPE_IFD_0
|
||||
&& (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
|
||||
if (ifdOffset > 0) {
|
||||
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needToParseOffsetsInCurrentIfd() {
|
||||
switch (mIfdType) {
|
||||
case IfdId.TYPE_IFD_0:
|
||||
return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS)
|
||||
|| isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
|
||||
|| isIfdRequested(IfdId.TYPE_IFD_1);
|
||||
case IfdId.TYPE_IFD_1:
|
||||
return isThumbnailRequested();
|
||||
case IfdId.TYPE_IFD_EXIF:
|
||||
// The offset to interoperability IFD is located in Exif IFD
|
||||
return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link #next()} return {@link #EVENT_NEW_TAG} or
|
||||
* {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the
|
||||
* corresponding tag.
|
||||
* <p>
|
||||
* For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size
|
||||
* of the value is greater than 4 bytes. One should call
|
||||
* {@link ExifTag#hasValue()} to check if the tag contains value. If there
|
||||
* is no value,call {@link #registerForTagValue(ExifTag)} to have the parser
|
||||
* emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
|
||||
* pointed by the offset.
|
||||
* <p>
|
||||
* When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the
|
||||
* tag will have already been read except for tags of undefined type. For
|
||||
* tags of undefined type, call one of the read methods to get the value.
|
||||
*
|
||||
* @see #registerForTagValue(ExifTag)
|
||||
* @see #read(byte[])
|
||||
* @see #read(byte[], int, int)
|
||||
* @see #readLong()
|
||||
* @see #readRational()
|
||||
* @see #readString(int)
|
||||
* @see #readString(int, Charset)
|
||||
*/
|
||||
protected ExifTag getTag() {
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets number of tags in the current IFD area.
|
||||
*/
|
||||
protected int getTagCountInCurrentIfd() {
|
||||
return mNumOfTagInIfd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of current IFD.
|
||||
*
|
||||
* @see IfdId#TYPE_IFD_0
|
||||
* @see IfdId#TYPE_IFD_1
|
||||
* @see IfdId#TYPE_IFD_GPS
|
||||
* @see IfdId#TYPE_IFD_INTEROPERABILITY
|
||||
* @see IfdId#TYPE_IFD_EXIF
|
||||
*/
|
||||
protected int getCurrentIfd() {
|
||||
return mIfdType;
|
||||
}
|
||||
|
||||
/**
|
||||
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
|
||||
* get the index of this strip.
|
||||
*
|
||||
* @see #getStripCount()
|
||||
*/
|
||||
protected int getStripIndex() {
|
||||
return mImageEvent.stripIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
|
||||
* get the number of strip data.
|
||||
*
|
||||
* @see #getStripIndex()
|
||||
*/
|
||||
protected int getStripCount() {
|
||||
return mStripCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
|
||||
* get the strip size.
|
||||
*/
|
||||
protected int getStripSize() {
|
||||
if (mStripSizeTag == null)
|
||||
return 0;
|
||||
return (int) mStripSizeTag.getValueAt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get
|
||||
* the image data size.
|
||||
*/
|
||||
protected int getCompressedImageSize() {
|
||||
if (mJpegSizeTag == null) {
|
||||
return 0;
|
||||
}
|
||||
return (int) mJpegSizeTag.getValueAt(0);
|
||||
}
|
||||
|
||||
private void skipTo(int offset) throws IOException {
|
||||
mTiffStream.skipTo(offset);
|
||||
while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
|
||||
mCorrespondingEvent.pollFirstEntry();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may
|
||||
* not contain the value if the size of the value is greater than 4 bytes.
|
||||
* When the value is not available here, call this method so that the parser
|
||||
* will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
|
||||
* where the value is located.
|
||||
*
|
||||
* @see #EVENT_VALUE_OF_REGISTERED_TAG
|
||||
*/
|
||||
protected void registerForTagValue(ExifTag tag) {
|
||||
if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
|
||||
mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
|
||||
}
|
||||
}
|
||||
|
||||
private void registerIfd(int ifdType, long offset) {
|
||||
// Cast unsigned int to int since the offset is always smaller
|
||||
// than the size of APP1 (65536)
|
||||
mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
|
||||
}
|
||||
|
||||
private void registerCompressedImage(long offset) {
|
||||
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
|
||||
}
|
||||
|
||||
private void registerUncompressedStrip(int stripIndex, long offset) {
|
||||
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP
|
||||
, stripIndex));
|
||||
}
|
||||
|
||||
private ExifTag readTag() throws IOException, ExifInvalidFormatException {
|
||||
short tagId = mTiffStream.readShort();
|
||||
short dataFormat = mTiffStream.readShort();
|
||||
long numOfComp = mTiffStream.readUnsignedInt();
|
||||
if (numOfComp > Integer.MAX_VALUE) {
|
||||
throw new ExifInvalidFormatException(
|
||||
"Number of component is larger then Integer.MAX_VALUE");
|
||||
}
|
||||
// Some invalid image file contains invalid data type. Ignore those tags
|
||||
if (!ExifTag.isValidType(dataFormat)) {
|
||||
Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat));
|
||||
mTiffStream.skip(4);
|
||||
return null;
|
||||
}
|
||||
// TODO: handle numOfComp overflow
|
||||
ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType,
|
||||
((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
|
||||
int dataSize = tag.getDataSize();
|
||||
if (dataSize > 4) {
|
||||
long offset = mTiffStream.readUnsignedInt();
|
||||
if (offset > Integer.MAX_VALUE) {
|
||||
throw new ExifInvalidFormatException(
|
||||
"offset is larger then Integer.MAX_VALUE");
|
||||
}
|
||||
// Some invalid images put some undefined data before IFD0.
|
||||
// Read the data here.
|
||||
if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
|
||||
byte[] buf = new byte[(int) numOfComp];
|
||||
System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET,
|
||||
buf, 0, (int) numOfComp);
|
||||
tag.setValue(buf);
|
||||
} else {
|
||||
tag.setOffset((int) offset);
|
||||
}
|
||||
} else {
|
||||
boolean defCount = tag.hasDefinedCount();
|
||||
// Set defined count to 0 so we can add \0 to non-terminated strings
|
||||
tag.setHasDefinedCount(false);
|
||||
// Read value
|
||||
readFullTagValue(tag);
|
||||
tag.setHasDefinedCount(defCount);
|
||||
mTiffStream.skip(4 - dataSize);
|
||||
// Set the offset to the position of value.
|
||||
tag.setOffset(mTiffStream.getReadByteCount() - 4);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the tag, if the tag is one of the offset tag that points to the IFD
|
||||
* or image the caller is interested in, register the IFD or image.
|
||||
*/
|
||||
private void checkOffsetOrImageTag(ExifTag tag) {
|
||||
// Some invalid formattd image contains tag with 0 size.
|
||||
if (tag.getComponentCount() == 0) {
|
||||
return;
|
||||
}
|
||||
short tid = tag.getTagId();
|
||||
int ifd = tag.getIfd();
|
||||
if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
|
||||
if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
|
||||
|| isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
|
||||
registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
|
||||
}
|
||||
} else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
|
||||
if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
|
||||
registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
|
||||
}
|
||||
} else if (tid == TAG_INTEROPERABILITY_IFD
|
||||
&& checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
|
||||
if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
|
||||
registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
|
||||
}
|
||||
} else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
|
||||
&& checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
|
||||
if (isThumbnailRequested()) {
|
||||
registerCompressedImage(tag.getValueAt(0));
|
||||
}
|
||||
} else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
|
||||
&& checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
|
||||
if (isThumbnailRequested()) {
|
||||
mJpegSizeTag = tag;
|
||||
}
|
||||
} else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
|
||||
if (isThumbnailRequested()) {
|
||||
if (tag.hasValue()) {
|
||||
for (int i = 0; i < tag.getComponentCount(); i++) {
|
||||
if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
|
||||
registerUncompressedStrip(i, tag.getValueAt(i));
|
||||
} else {
|
||||
registerUncompressedStrip(i, tag.getValueAt(i));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
|
||||
}
|
||||
}
|
||||
} else if (tid == TAG_STRIP_BYTE_COUNTS
|
||||
&& checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
|
||||
&&isThumbnailRequested() && tag.hasValue()) {
|
||||
mStripSizeTag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAllowed(int ifd, int tagId) {
|
||||
int info = mInterface.getTagInfo().get(tagId);
|
||||
if (info == ExifInterface.DEFINITION_NULL) {
|
||||
return false;
|
||||
}
|
||||
return ExifInterface.isIfdAllowed(info, ifd);
|
||||
}
|
||||
|
||||
protected void readFullTagValue(ExifTag tag) throws IOException {
|
||||
// Some invalid images contains tags with wrong size, check it here
|
||||
short type = tag.getDataType();
|
||||
if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
|
||||
type == ExifTag.TYPE_UNSIGNED_BYTE) {
|
||||
int size = tag.getComponentCount();
|
||||
if (mCorrespondingEvent.size() > 0) {
|
||||
if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount()
|
||||
+ size) {
|
||||
Object event = mCorrespondingEvent.firstEntry().getValue();
|
||||
if (event instanceof ImageEvent) {
|
||||
// Tag value overlaps thumbnail, ignore thumbnail.
|
||||
Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString());
|
||||
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
|
||||
Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
|
||||
} else {
|
||||
// Tag value overlaps another tag, shorten count
|
||||
if (event instanceof IfdEvent) {
|
||||
Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd
|
||||
+ " overlaps value for tag: \n" + tag.toString());
|
||||
} else if (event instanceof ExifTagEvent) {
|
||||
Log.w(TAG, "Tag value for tag: \n"
|
||||
+ ((ExifTagEvent) event).tag.toString()
|
||||
+ " overlaps value for tag: \n" + tag.toString());
|
||||
}
|
||||
size = mCorrespondingEvent.firstEntry().getKey()
|
||||
- mTiffStream.getReadByteCount();
|
||||
Log.w(TAG, "Invalid size of tag: \n" + tag.toString()
|
||||
+ " setting count to: " + size);
|
||||
tag.forceSetComponentCount(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (tag.getDataType()) {
|
||||
case ExifTag.TYPE_UNSIGNED_BYTE:
|
||||
case ExifTag.TYPE_UNDEFINED: {
|
||||
byte buf[] = new byte[tag.getComponentCount()];
|
||||
read(buf);
|
||||
tag.setValue(buf);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_ASCII:
|
||||
tag.setValue(readString(tag.getComponentCount()));
|
||||
break;
|
||||
case ExifTag.TYPE_UNSIGNED_LONG: {
|
||||
long value[] = new long[tag.getComponentCount()];
|
||||
for (int i = 0, n = value.length; i < n; i++) {
|
||||
value[i] = readUnsignedLong();
|
||||
}
|
||||
tag.setValue(value);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_UNSIGNED_RATIONAL: {
|
||||
Rational value[] = new Rational[tag.getComponentCount()];
|
||||
for (int i = 0, n = value.length; i < n; i++) {
|
||||
value[i] = readUnsignedRational();
|
||||
}
|
||||
tag.setValue(value);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_UNSIGNED_SHORT: {
|
||||
int value[] = new int[tag.getComponentCount()];
|
||||
for (int i = 0, n = value.length; i < n; i++) {
|
||||
value[i] = readUnsignedShort();
|
||||
}
|
||||
tag.setValue(value);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_LONG: {
|
||||
int value[] = new int[tag.getComponentCount()];
|
||||
for (int i = 0, n = value.length; i < n; i++) {
|
||||
value[i] = readLong();
|
||||
}
|
||||
tag.setValue(value);
|
||||
}
|
||||
break;
|
||||
case ExifTag.TYPE_RATIONAL: {
|
||||
Rational value[] = new Rational[tag.getComponentCount()];
|
||||
for (int i = 0, n = value.length; i < n; i++) {
|
||||
value[i] = readRational();
|
||||
}
|
||||
tag.setValue(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (LOGV) {
|
||||
Log.v(TAG, "\n" + tag.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseTiffHeader() throws IOException,
|
||||
ExifInvalidFormatException {
|
||||
short byteOrder = mTiffStream.readShort();
|
||||
if (LITTLE_ENDIAN_TAG == byteOrder) {
|
||||
mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
} else if (BIG_ENDIAN_TAG == byteOrder) {
|
||||
mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
|
||||
} else {
|
||||
throw new ExifInvalidFormatException("Invalid TIFF header");
|
||||
}
|
||||
|
||||
if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
|
||||
throw new ExifInvalidFormatException("Invalid TIFF header");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean seekTiffData(InputStream inputStream) throws IOException,
|
||||
ExifInvalidFormatException {
|
||||
CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
|
||||
if (dataStream.readShort() != JpegHeader.SOI) {
|
||||
throw new ExifInvalidFormatException("Invalid JPEG format");
|
||||
}
|
||||
|
||||
short marker = dataStream.readShort();
|
||||
while (marker != JpegHeader.EOI
|
||||
&& !JpegHeader.isSofMarker(marker)) {
|
||||
int length = dataStream.readUnsignedShort();
|
||||
// Some invalid formatted image contains multiple APP1,
|
||||
// try to find the one with Exif data.
|
||||
if (marker == JpegHeader.APP1) {
|
||||
int header = 0;
|
||||
short headerTail = 0;
|
||||
if (length >= 8) {
|
||||
header = dataStream.readInt();
|
||||
headerTail = dataStream.readShort();
|
||||
length -= 6;
|
||||
if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
|
||||
mTiffStartPosition = dataStream.getReadByteCount();
|
||||
mApp1End = length;
|
||||
mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
|
||||
Log.w(TAG, "Invalid JPEG format.");
|
||||
return false;
|
||||
}
|
||||
marker = dataStream.readShort();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected int getOffsetToExifEndFromSOF() {
|
||||
return mOffsetToApp1EndFromSOF;
|
||||
}
|
||||
|
||||
protected int getTiffStartPosition() {
|
||||
return mTiffStartPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads bytes from the InputStream.
|
||||
*/
|
||||
protected int read(byte[] buffer, int offset, int length) throws IOException {
|
||||
return mTiffStream.read(buffer, offset, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to read(buffer, 0, buffer.length).
|
||||
*/
|
||||
protected int read(byte[] buffer) throws IOException {
|
||||
return mTiffStream.read(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a String from the InputStream with US-ASCII charset. The parser
|
||||
* will read n bytes and convert it to ascii string. This is used for
|
||||
* reading values of type {@link ExifTag#TYPE_ASCII}.
|
||||
*/
|
||||
protected String readString(int n) throws IOException {
|
||||
return readString(n, US_ASCII);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a String from the InputStream with the given charset. The parser
|
||||
* will read n bytes and convert it to string. This is used for reading
|
||||
* values of type {@link ExifTag#TYPE_ASCII}.
|
||||
*/
|
||||
protected String readString(int n, Charset charset) throws IOException {
|
||||
if (n > 0) {
|
||||
return mTiffStream.readString(n, charset);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
|
||||
* InputStream.
|
||||
*/
|
||||
protected int readUnsignedShort() throws IOException {
|
||||
return mTiffStream.readShort() & 0xffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
|
||||
* InputStream.
|
||||
*/
|
||||
protected long readUnsignedLong() throws IOException {
|
||||
return readLong() & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
|
||||
* InputStream.
|
||||
*/
|
||||
protected Rational readUnsignedRational() throws IOException {
|
||||
long nomi = readUnsignedLong();
|
||||
long denomi = readUnsignedLong();
|
||||
return new Rational(nomi, denomi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
|
||||
*/
|
||||
protected int readLong() throws IOException {
|
||||
return mTiffStream.readInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
|
||||
*/
|
||||
protected Rational readRational() throws IOException {
|
||||
int nomi = readLong();
|
||||
int denomi = readLong();
|
||||
return new Rational(nomi, denomi);
|
||||
}
|
||||
|
||||
private static class ImageEvent {
|
||||
int stripIndex;
|
||||
int type;
|
||||
|
||||
ImageEvent(int type) {
|
||||
this.stripIndex = 0;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
ImageEvent(int type, int stripIndex) {
|
||||
this.type = type;
|
||||
this.stripIndex = stripIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private static class IfdEvent {
|
||||
int ifd;
|
||||
boolean isRequested;
|
||||
|
||||
IfdEvent(int ifd, boolean isInterestedIfd) {
|
||||
this.ifd = ifd;
|
||||
this.isRequested = isInterestedIfd;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExifTagEvent {
|
||||
ExifTag tag;
|
||||
boolean isRequested;
|
||||
|
||||
ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
|
||||
this.tag = tag;
|
||||
this.isRequested = isRequireByUser;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the byte order of the current InputStream.
|
||||
*/
|
||||
protected ByteOrder getByteOrder() {
|
||||
return mTiffStream.getByteOrder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* This class reads the EXIF header of a JPEG file and stores it in
|
||||
* {@link ExifData}.
|
||||
*/
|
||||
class ExifReader {
|
||||
private static final String TAG = "ExifReader";
|
||||
|
||||
private final ExifInterface mInterface;
|
||||
|
||||
ExifReader(ExifInterface iRef) {
|
||||
mInterface = iRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the inputStream and and returns the EXIF data in an
|
||||
* {@link ExifData}.
|
||||
*
|
||||
* @throws ExifInvalidFormatException
|
||||
* @throws IOException
|
||||
*/
|
||||
protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
|
||||
IOException {
|
||||
ExifParser parser = ExifParser.parse(inputStream, mInterface);
|
||||
ExifData exifData = new ExifData(parser.getByteOrder());
|
||||
ExifTag tag = null;
|
||||
|
||||
int event = parser.next();
|
||||
while (event != ExifParser.EVENT_END) {
|
||||
switch (event) {
|
||||
case ExifParser.EVENT_START_OF_IFD:
|
||||
exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
|
||||
break;
|
||||
case ExifParser.EVENT_NEW_TAG:
|
||||
tag = parser.getTag();
|
||||
if (!tag.hasValue()) {
|
||||
parser.registerForTagValue(tag);
|
||||
} else {
|
||||
exifData.getIfdData(tag.getIfd()).setTag(tag);
|
||||
}
|
||||
break;
|
||||
case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
|
||||
tag = parser.getTag();
|
||||
if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
|
||||
parser.readFullTagValue(tag);
|
||||
}
|
||||
exifData.getIfdData(tag.getIfd()).setTag(tag);
|
||||
break;
|
||||
case ExifParser.EVENT_COMPRESSED_IMAGE:
|
||||
byte buf[] = new byte[parser.getCompressedImageSize()];
|
||||
if (buf.length == parser.read(buf)) {
|
||||
exifData.setCompressedThumbnail(buf);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to read the compressed thumbnail");
|
||||
}
|
||||
break;
|
||||
case ExifParser.EVENT_UNCOMPRESSED_STRIP:
|
||||
buf = new byte[parser.getStripSize()];
|
||||
if (buf.length == parser.read(buf)) {
|
||||
exifData.setStripBytes(parser.getStripIndex(), buf);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to read the strip bytes");
|
||||
}
|
||||
break;
|
||||
}
|
||||
event = parser.next();
|
||||
}
|
||||
return exifData;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class stores all the tags in an IFD.
|
||||
*
|
||||
* @see ExifData
|
||||
* @see ExifTag
|
||||
*/
|
||||
class IfdData {
|
||||
|
||||
private final int mIfdId;
|
||||
private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
|
||||
private int mOffsetToNextIfd = 0;
|
||||
private static final int[] sIfds = {
|
||||
IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF,
|
||||
IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS
|
||||
};
|
||||
/**
|
||||
* Creates an IfdData with given IFD ID.
|
||||
*
|
||||
* @see IfdId#TYPE_IFD_0
|
||||
* @see IfdId#TYPE_IFD_1
|
||||
* @see IfdId#TYPE_IFD_EXIF
|
||||
* @see IfdId#TYPE_IFD_GPS
|
||||
* @see IfdId#TYPE_IFD_INTEROPERABILITY
|
||||
*/
|
||||
IfdData(int ifdId) {
|
||||
mIfdId = ifdId;
|
||||
}
|
||||
|
||||
static protected int[] getIfds() {
|
||||
return sIfds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a array the contains all {@link ExifTag} in this IFD.
|
||||
*/
|
||||
protected ExifTag[] getAllTags() {
|
||||
return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of this IFD.
|
||||
*
|
||||
* @see IfdId#TYPE_IFD_0
|
||||
* @see IfdId#TYPE_IFD_1
|
||||
* @see IfdId#TYPE_IFD_EXIF
|
||||
* @see IfdId#TYPE_IFD_GPS
|
||||
* @see IfdId#TYPE_IFD_INTEROPERABILITY
|
||||
*/
|
||||
protected int getId() {
|
||||
return mIfdId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ExifTag} with given tag id. Return null if there is no
|
||||
* such tag.
|
||||
*/
|
||||
protected ExifTag getTag(short tagId) {
|
||||
return mExifTags.get(tagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or replaces a {@link ExifTag}.
|
||||
*/
|
||||
protected ExifTag setTag(ExifTag tag) {
|
||||
tag.setIfd(mIfdId);
|
||||
return mExifTags.put(tag.getTagId(), tag);
|
||||
}
|
||||
|
||||
protected boolean checkCollision(short tagId) {
|
||||
return mExifTags.get(tagId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the tag of the given ID
|
||||
*/
|
||||
protected void removeTag(short tagId) {
|
||||
mExifTags.remove(tagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tags count in the IFD.
|
||||
*/
|
||||
protected int getTagCount() {
|
||||
return mExifTags.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of next IFD.
|
||||
*/
|
||||
protected void setOffsetToNextIfd(int offset) {
|
||||
mOffsetToNextIfd = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the offset of next IFD.
|
||||
*/
|
||||
protected int getOffsetToNextIfd() {
|
||||
return mOffsetToNextIfd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all tags in this two IFDs are equal. Note that tags of
|
||||
* IFDs offset or thumbnail offset will be ignored.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (obj instanceof IfdData) {
|
||||
IfdData data = (IfdData) obj;
|
||||
if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
|
||||
ExifTag[] tags = data.getAllTags();
|
||||
for (ExifTag tag : tags) {
|
||||
if (ExifInterface.isOffsetTag(tag.getTagId())) {
|
||||
continue;
|
||||
}
|
||||
ExifTag tag2 = mExifTags.get(tag.getTagId());
|
||||
if (!tag.equals(tag2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
/**
|
||||
* The constants of the IFD ID defined in EXIF spec.
|
||||
*/
|
||||
public interface IfdId {
|
||||
public static final int TYPE_IFD_0 = 0;
|
||||
public static final int TYPE_IFD_1 = 1;
|
||||
public static final int TYPE_IFD_EXIF = 2;
|
||||
public static final int TYPE_IFD_INTEROPERABILITY = 3;
|
||||
public static final int TYPE_IFD_GPS = 4;
|
||||
/* This is used in ExifData to allocate enough IfdData */
|
||||
static final int TYPE_IFD_COUNT = 5;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
class JpegHeader {
|
||||
public static final short SOI = (short) 0xFFD8;
|
||||
public static final short APP1 = (short) 0xFFE1;
|
||||
public static final short APP0 = (short) 0xFFE0;
|
||||
public static final short EOI = (short) 0xFFD9;
|
||||
|
||||
/**
|
||||
* SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG,
|
||||
* and DAC marker.
|
||||
*/
|
||||
public static final short SOF0 = (short) 0xFFC0;
|
||||
public static final short SOF15 = (short) 0xFFCF;
|
||||
public static final short DHT = (short) 0xFFC4;
|
||||
public static final short JPG = (short) 0xFFC8;
|
||||
public static final short DAC = (short) 0xFFCC;
|
||||
|
||||
public static final boolean isSofMarker(short marker) {
|
||||
return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
|
||||
&& marker != DAC;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
class OrderedDataOutputStream extends FilterOutputStream {
|
||||
private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
|
||||
|
||||
public OrderedDataOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
public OrderedDataOutputStream setByteOrder(ByteOrder order) {
|
||||
mByteBuffer.order(order);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OrderedDataOutputStream writeShort(short value) throws IOException {
|
||||
mByteBuffer.rewind();
|
||||
mByteBuffer.putShort(value);
|
||||
out.write(mByteBuffer.array(), 0, 2);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OrderedDataOutputStream writeInt(int value) throws IOException {
|
||||
mByteBuffer.rewind();
|
||||
mByteBuffer.putInt(value);
|
||||
out.write(mByteBuffer.array());
|
||||
return this;
|
||||
}
|
||||
|
||||
public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
|
||||
writeInt((int) rational.getNumerator());
|
||||
writeInt((int) rational.getDenominator());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.exif;
|
||||
|
||||
/**
|
||||
* The rational data type of EXIF tag. Contains a pair of longs representing the
|
||||
* numerator and denominator of a Rational number.
|
||||
*/
|
||||
public class Rational {
|
||||
|
||||
private final long mNumerator;
|
||||
private final long mDenominator;
|
||||
|
||||
/**
|
||||
* Create a Rational with a given numerator and denominator.
|
||||
*
|
||||
* @param nominator
|
||||
* @param denominator
|
||||
*/
|
||||
public Rational(long nominator, long denominator) {
|
||||
mNumerator = nominator;
|
||||
mDenominator = denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of a Rational.
|
||||
*/
|
||||
public Rational(Rational r) {
|
||||
mNumerator = r.mNumerator;
|
||||
mDenominator = r.mDenominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the numerator of the rational.
|
||||
*/
|
||||
public long getNumerator() {
|
||||
return mNumerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the denominator of the rational
|
||||
*/
|
||||
public long getDenominator() {
|
||||
return mDenominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rational value as type double. Will cause a divide-by-zero error
|
||||
* if the denominator is 0.
|
||||
*/
|
||||
public double toDouble() {
|
||||
return mNumerator / (double) mDenominator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof Rational) {
|
||||
Rational data = (Rational) obj;
|
||||
return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mNumerator + "/" + mDenominator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.gallery3d.common.Utils;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
// BasicTexture is a Texture corresponds to a real GL texture.
|
||||
// The state of a BasicTexture indicates whether its data is loaded to GL memory.
|
||||
// If a BasicTexture is loaded into GL memory, it has a GL texture id.
|
||||
public abstract class BasicTexture implements Texture {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = "BasicTexture";
|
||||
protected static final int UNSPECIFIED = -1;
|
||||
|
||||
protected static final int STATE_UNLOADED = 0;
|
||||
protected static final int STATE_LOADED = 1;
|
||||
protected static final int STATE_ERROR = -1;
|
||||
|
||||
// Log a warning if a texture is larger along a dimension
|
||||
private static final int MAX_TEXTURE_SIZE = 4096;
|
||||
|
||||
protected int mId = -1;
|
||||
protected int mState;
|
||||
|
||||
protected int mWidth = UNSPECIFIED;
|
||||
protected int mHeight = UNSPECIFIED;
|
||||
|
||||
protected int mTextureWidth;
|
||||
protected int mTextureHeight;
|
||||
|
||||
private boolean mHasBorder;
|
||||
|
||||
protected GLCanvas mCanvasRef = null;
|
||||
private static WeakHashMap<BasicTexture, Object> sAllTextures
|
||||
= new WeakHashMap<BasicTexture, Object>();
|
||||
private static ThreadLocal sInFinalizer = new ThreadLocal();
|
||||
|
||||
protected BasicTexture(GLCanvas canvas, int id, int state) {
|
||||
setAssociatedCanvas(canvas);
|
||||
mId = id;
|
||||
mState = state;
|
||||
synchronized (sAllTextures) {
|
||||
sAllTextures.put(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected BasicTexture() {
|
||||
this(null, 0, STATE_UNLOADED);
|
||||
}
|
||||
|
||||
protected void setAssociatedCanvas(GLCanvas canvas) {
|
||||
mCanvasRef = canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content size of this texture. In OpenGL, the actual texture
|
||||
* size must be of power of 2, the size of the content may be smaller.
|
||||
*/
|
||||
public void setSize(int width, int height) {
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mTextureWidth = width > 0 ? Utils.nextPowerOf2(width) : 0;
|
||||
mTextureHeight = height > 0 ? Utils.nextPowerOf2(height) : 0;
|
||||
if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
|
||||
Log.w(TAG, String.format("texture is too large: %d x %d",
|
||||
mTextureWidth, mTextureHeight), new Exception());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFlippedVertically() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
// Returns the width rounded to the next power of 2.
|
||||
public int getTextureWidth() {
|
||||
return mTextureWidth;
|
||||
}
|
||||
|
||||
// Returns the height rounded to the next power of 2.
|
||||
public int getTextureHeight() {
|
||||
return mTextureHeight;
|
||||
}
|
||||
|
||||
// Returns true if the texture has one pixel transparent border around the
|
||||
// actual content. This is used to avoid jigged edges.
|
||||
//
|
||||
// The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
|
||||
// mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
|
||||
// covered by the texture will use the color of the edge texel. If we add
|
||||
// the transparent border, the color of the edge texel will be mixed with
|
||||
// appropriate amount of transparent.
|
||||
//
|
||||
// Currently our background is black, so we can draw the thumbnails without
|
||||
// enabling blending.
|
||||
public boolean hasBorder() {
|
||||
return mHasBorder;
|
||||
}
|
||||
|
||||
protected void setBorder(boolean hasBorder) {
|
||||
mHasBorder = hasBorder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y) {
|
||||
canvas.drawTexture(this, x, y, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
|
||||
canvas.drawTexture(this, x, y, w, h);
|
||||
}
|
||||
|
||||
// onBind is called before GLCanvas binds this texture.
|
||||
// It should make sure the data is uploaded to GL memory.
|
||||
abstract protected boolean onBind(GLCanvas canvas);
|
||||
|
||||
// Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
|
||||
abstract protected int getTarget();
|
||||
|
||||
public boolean isLoaded() {
|
||||
return mState == STATE_LOADED;
|
||||
}
|
||||
|
||||
// recycle() is called when the texture will never be used again,
|
||||
// so it can free all resources.
|
||||
public void recycle() {
|
||||
freeResource();
|
||||
}
|
||||
|
||||
// yield() is called when the texture will not be used temporarily,
|
||||
// so it can free some resources.
|
||||
// The default implementation unloads the texture from GL memory, so
|
||||
// the subclass should make sure it can reload the texture to GL memory
|
||||
// later, or it will have to override this method.
|
||||
public void yield() {
|
||||
freeResource();
|
||||
}
|
||||
|
||||
private void freeResource() {
|
||||
GLCanvas canvas = mCanvasRef;
|
||||
if (canvas != null && mId != -1) {
|
||||
canvas.unloadTexture(this);
|
||||
mId = -1; // Don't free it again.
|
||||
}
|
||||
mState = STATE_UNLOADED;
|
||||
setAssociatedCanvas(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() {
|
||||
sInFinalizer.set(BasicTexture.class);
|
||||
recycle();
|
||||
sInFinalizer.set(null);
|
||||
}
|
||||
|
||||
// This is for deciding if we can call Bitmap's recycle().
|
||||
// We cannot call Bitmap's recycle() in finalizer because at that point
|
||||
// the finalizer of Bitmap may already be called so recycle() will crash.
|
||||
public static boolean inFinalizer() {
|
||||
return sInFinalizer.get() != null;
|
||||
}
|
||||
|
||||
public static void yieldAllTextures() {
|
||||
synchronized (sAllTextures) {
|
||||
for (BasicTexture t : sAllTextures.keySet()) {
|
||||
t.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidateAllTextures() {
|
||||
synchronized (sAllTextures) {
|
||||
for (BasicTexture t : sAllTextures.keySet()) {
|
||||
t.mState = STATE_UNLOADED;
|
||||
t.setAssociatedCanvas(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
|
||||
//
|
||||
// The texture does not own the Bitmap. The user should make sure the Bitmap
|
||||
// is valid during the texture's lifetime. When the texture is recycled, it
|
||||
// does not free the Bitmap.
|
||||
public class BitmapTexture extends UploadedTexture {
|
||||
protected Bitmap mContentBitmap;
|
||||
|
||||
public BitmapTexture(Bitmap bitmap) {
|
||||
this(bitmap, false);
|
||||
}
|
||||
|
||||
public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
|
||||
super(hasBorder);
|
||||
Assert.assertTrue(bitmap != null && !bitmap.isRecycled());
|
||||
mContentBitmap = bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFreeBitmap(Bitmap bitmap) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap onGetBitmap() {
|
||||
return mContentBitmap;
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return mContentBitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL11;
|
||||
|
||||
//
|
||||
// GLCanvas gives a convenient interface to draw using OpenGL.
|
||||
//
|
||||
// When a rectangle is specified in this interface, it means the region
|
||||
// [x, x+width) * [y, y+height)
|
||||
//
|
||||
public interface GLCanvas {
|
||||
|
||||
public GLId getGLId();
|
||||
|
||||
// Tells GLCanvas the size of the underlying GL surface. This should be
|
||||
// called before first drawing and when the size of GL surface is changed.
|
||||
// This is called by GLRoot and should not be called by the clients
|
||||
// who only want to draw on the GLCanvas. Both width and height must be
|
||||
// nonnegative.
|
||||
public abstract void setSize(int width, int height);
|
||||
|
||||
// Clear the drawing buffers. This should only be used by GLRoot.
|
||||
public abstract void clearBuffer();
|
||||
|
||||
public abstract void clearBuffer(float[] argb);
|
||||
|
||||
// Sets and gets the current alpha, alpha must be in [0, 1].
|
||||
public abstract void setAlpha(float alpha);
|
||||
|
||||
public abstract float getAlpha();
|
||||
|
||||
// (current alpha) = (current alpha) * alpha
|
||||
public abstract void multiplyAlpha(float alpha);
|
||||
|
||||
// Change the current transform matrix.
|
||||
public abstract void translate(float x, float y, float z);
|
||||
|
||||
public abstract void translate(float x, float y);
|
||||
|
||||
public abstract void scale(float sx, float sy, float sz);
|
||||
|
||||
public abstract void rotate(float angle, float x, float y, float z);
|
||||
|
||||
public abstract void multiplyMatrix(float[] mMatrix, int offset);
|
||||
|
||||
// Pushes the configuration state (matrix, and alpha) onto
|
||||
// a private stack.
|
||||
public abstract void save();
|
||||
|
||||
// Same as save(), but only save those specified in saveFlags.
|
||||
public abstract void save(int saveFlags);
|
||||
|
||||
public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
|
||||
public static final int SAVE_FLAG_ALPHA = 0x01;
|
||||
public static final int SAVE_FLAG_MATRIX = 0x02;
|
||||
|
||||
// Pops from the top of the stack as current configuration state (matrix,
|
||||
// alpha, and clip). This call balances a previous call to save(), and is
|
||||
// used to remove all modifications to the configuration state since the
|
||||
// last save call.
|
||||
public abstract void restore();
|
||||
|
||||
// Draws a line using the specified paint from (x1, y1) to (x2, y2).
|
||||
// (Both end points are included).
|
||||
public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
|
||||
|
||||
// Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
|
||||
// (Both end points are included).
|
||||
public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
|
||||
|
||||
// Fills the specified rectangle with the specified color.
|
||||
public abstract void fillRect(float x, float y, float width, float height, int color);
|
||||
|
||||
// Draws a texture to the specified rectangle.
|
||||
public abstract void drawTexture(
|
||||
BasicTexture texture, int x, int y, int width, int height);
|
||||
|
||||
public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
|
||||
int uvBuffer, int indexBuffer, int indexCount);
|
||||
|
||||
// Draws the source rectangle part of the texture to the target rectangle.
|
||||
public abstract void drawTexture(BasicTexture texture, RectF source, RectF target);
|
||||
|
||||
// Draw a texture with a specified texture transform.
|
||||
public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform,
|
||||
int x, int y, int w, int h);
|
||||
|
||||
// Draw two textures to the specified rectangle. The actual texture used is
|
||||
// from * (1 - ratio) + to * ratio
|
||||
// The two textures must have the same size.
|
||||
public abstract void drawMixed(BasicTexture from, int toColor,
|
||||
float ratio, int x, int y, int w, int h);
|
||||
|
||||
// Draw a region of a texture and a specified color to the specified
|
||||
// rectangle. The actual color used is from * (1 - ratio) + to * ratio.
|
||||
// The region of the texture is defined by parameter "src". The target
|
||||
// rectangle is specified by parameter "target".
|
||||
public abstract void drawMixed(BasicTexture from, int toColor,
|
||||
float ratio, RectF src, RectF target);
|
||||
|
||||
// Unloads the specified texture from the canvas. The resource allocated
|
||||
// to draw the texture will be released. The specified texture will return
|
||||
// to the unloaded state. This function should be called only from
|
||||
// BasicTexture or its descendant
|
||||
public abstract boolean unloadTexture(BasicTexture texture);
|
||||
|
||||
// Delete the specified buffer object, similar to unloadTexture.
|
||||
public abstract void deleteBuffer(int bufferId);
|
||||
|
||||
// Delete the textures and buffers in GL side. This function should only be
|
||||
// called in the GL thread.
|
||||
public abstract void deleteRecycledResources();
|
||||
|
||||
// Dump statistics information and clear the counters. For debug only.
|
||||
public abstract void dumpStatisticsAndClear();
|
||||
|
||||
public abstract void beginRenderTarget(RawTexture texture);
|
||||
|
||||
public abstract void endRenderTarget();
|
||||
|
||||
/**
|
||||
* Sets texture parameters to use GL_CLAMP_TO_EDGE for both
|
||||
* GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be
|
||||
* GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.
|
||||
* bindTexture() must be called prior to this.
|
||||
*
|
||||
* @param texture The texture to set parameters on.
|
||||
*/
|
||||
public abstract void setTextureParameters(BasicTexture texture);
|
||||
|
||||
/**
|
||||
* Initializes the texture to a size by calling texImage2D on it.
|
||||
*
|
||||
* @param texture The texture to initialize the size.
|
||||
* @param format The texture format (e.g. GL_RGBA)
|
||||
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
|
||||
*/
|
||||
public abstract void initializeTextureSize(BasicTexture texture, int format, int type);
|
||||
|
||||
/**
|
||||
* Initializes the texture to a size by calling texImage2D on it.
|
||||
*
|
||||
* @param texture The texture to initialize the size.
|
||||
* @param bitmap The bitmap to initialize the bitmap with.
|
||||
*/
|
||||
public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap);
|
||||
|
||||
/**
|
||||
* Calls glTexSubImage2D to upload a bitmap to the texture.
|
||||
*
|
||||
* @param texture The target texture to write to.
|
||||
* @param xOffset Specifies a texel offset in the x direction within the
|
||||
* texture array.
|
||||
* @param yOffset Specifies a texel offset in the y direction within the
|
||||
* texture array.
|
||||
* @param format The texture format (e.g. GL_RGBA)
|
||||
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
|
||||
*/
|
||||
public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
|
||||
Bitmap bitmap,
|
||||
int format, int type);
|
||||
|
||||
/**
|
||||
* Generates buffers and uploads the buffer data.
|
||||
*
|
||||
* @param buffer The buffer to upload
|
||||
* @return The buffer ID that was generated.
|
||||
*/
|
||||
public abstract int uploadBuffer(java.nio.FloatBuffer buffer);
|
||||
|
||||
/**
|
||||
* Generates buffers and uploads the element array buffer data.
|
||||
*
|
||||
* @param buffer The buffer to upload
|
||||
* @return The buffer ID that was generated.
|
||||
*/
|
||||
public abstract int uploadBuffer(java.nio.ByteBuffer buffer);
|
||||
|
||||
/**
|
||||
* After LightCycle makes GL calls, this method is called to restore the GL
|
||||
* configuration to the one expected by GLCanvas.
|
||||
*/
|
||||
public abstract void recoverFromLightCycle();
|
||||
|
||||
/**
|
||||
* Gets the bounds given by x, y, width, and height as well as the internal
|
||||
* matrix state. There is no special handling for non-90-degree rotations.
|
||||
* It only considers the lower-left and upper-right corners as the bounds.
|
||||
*
|
||||
* @param bounds The output bounds to write to.
|
||||
* @param x The left side of the input rectangle.
|
||||
* @param y The bottom of the input rectangle.
|
||||
* @param width The width of the input rectangle.
|
||||
* @param height The height of the input rectangle.
|
||||
*/
|
||||
public abstract void getBounds(Rect bounds, int x, int y, int width, int height);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL11;
|
||||
import javax.microedition.khronos.opengles.GL11ExtensionPack;
|
||||
|
||||
public class GLES20IdImpl implements GLId {
|
||||
private final int[] mTempIntArray = new int[1];
|
||||
|
||||
@Override
|
||||
public int generateTexture() {
|
||||
GLES20.glGenTextures(1, mTempIntArray, 0);
|
||||
GLES20Canvas.checkError();
|
||||
return mTempIntArray[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glGenBuffers(int n, int[] buffers, int offset) {
|
||||
GLES20.glGenBuffers(n, buffers, offset);
|
||||
GLES20Canvas.checkError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
|
||||
GLES20.glDeleteTextures(n, textures, offset);
|
||||
GLES20Canvas.checkError();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
|
||||
GLES20.glDeleteBuffers(n, buffers, offset);
|
||||
GLES20Canvas.checkError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
|
||||
GLES20.glDeleteFramebuffers(n, buffers, offset);
|
||||
GLES20Canvas.checkError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL11;
|
||||
import javax.microedition.khronos.opengles.GL11ExtensionPack;
|
||||
|
||||
// This mimics corresponding GL functions.
|
||||
public interface GLId {
|
||||
public int generateTexture();
|
||||
|
||||
public void glGenBuffers(int n, int[] buffers, int offset);
|
||||
|
||||
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);
|
||||
|
||||
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);
|
||||
|
||||
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
public class GLPaint {
|
||||
private float mLineWidth = 1f;
|
||||
private int mColor = 0;
|
||||
|
||||
public void setColor(int color) {
|
||||
mColor = color;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return mColor;
|
||||
}
|
||||
|
||||
public void setLineWidth(float width) {
|
||||
Assert.assertTrue(width >= 0);
|
||||
mLineWidth = width;
|
||||
}
|
||||
|
||||
public float getLineWidth() {
|
||||
return mLineWidth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL11;
|
||||
|
||||
public class RawTexture extends BasicTexture {
|
||||
private static final String TAG = "RawTexture";
|
||||
|
||||
private final boolean mOpaque;
|
||||
private boolean mIsFlipped;
|
||||
|
||||
public RawTexture(int width, int height, boolean opaque) {
|
||||
mOpaque = opaque;
|
||||
setSize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaque() {
|
||||
return mOpaque;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFlippedVertically() {
|
||||
return mIsFlipped;
|
||||
}
|
||||
|
||||
public void setIsFlippedVertically(boolean isFlipped) {
|
||||
mIsFlipped = isFlipped;
|
||||
}
|
||||
|
||||
protected void prepare(GLCanvas canvas) {
|
||||
GLId glId = canvas.getGLId();
|
||||
mId = glId.generateTexture();
|
||||
canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
|
||||
canvas.setTextureParameters(this);
|
||||
mState = STATE_LOADED;
|
||||
setAssociatedCanvas(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onBind(GLCanvas canvas) {
|
||||
if (isLoaded()) return true;
|
||||
Log.w(TAG, "lost the content due to context change");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void yield() {
|
||||
// we cannot free the texture because we have no backup.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTarget() {
|
||||
return GL11.GL_TEXTURE_2D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
|
||||
// Texture is a rectangular image which can be drawn on GLCanvas.
|
||||
// The isOpaque() function gives a hint about whether the texture is opaque,
|
||||
// so the drawing can be done faster.
|
||||
//
|
||||
// This is the current texture hierarchy:
|
||||
//
|
||||
// Texture
|
||||
// -- ColorTexture
|
||||
// -- FadeInTexture
|
||||
// -- BasicTexture
|
||||
// -- UploadedTexture
|
||||
// -- BitmapTexture
|
||||
// -- Tile
|
||||
// -- ResourceTexture
|
||||
// -- NinePatchTexture
|
||||
// -- CanvasTexture
|
||||
// -- StringTexture
|
||||
//
|
||||
public interface Texture {
|
||||
public int getWidth();
|
||||
public int getHeight();
|
||||
public void draw(GLCanvas canvas, int x, int y);
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h);
|
||||
public boolean isOpaque();
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.glrenderer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.opengl.GLUtils;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL11;
|
||||
|
||||
// UploadedTextures use a Bitmap for the content of the texture.
|
||||
//
|
||||
// Subclasses should implement onGetBitmap() to provide the Bitmap and
|
||||
// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
|
||||
// is not needed anymore.
|
||||
//
|
||||
// isContentValid() is meaningful only when the isLoaded() returns true.
|
||||
// It means whether the content needs to be updated.
|
||||
//
|
||||
// The user of this class should call recycle() when the texture is not
|
||||
// needed anymore.
|
||||
//
|
||||
// By default an UploadedTexture is opaque (so it can be drawn faster without
|
||||
// blending). The user or subclass can override it using setOpaque().
|
||||
public abstract class UploadedTexture extends BasicTexture {
|
||||
|
||||
// To prevent keeping allocation the borders, we store those used borders here.
|
||||
// Since the length will be power of two, it won't use too much memory.
|
||||
private static HashMap<BorderKey, Bitmap> sBorderLines =
|
||||
new HashMap<BorderKey, Bitmap>();
|
||||
private static BorderKey sBorderKey = new BorderKey();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = "Texture";
|
||||
private boolean mContentValid = true;
|
||||
|
||||
// indicate this textures is being uploaded in background
|
||||
private boolean mIsUploading = false;
|
||||
private boolean mOpaque = true;
|
||||
private boolean mThrottled = false;
|
||||
private static int sUploadedCount;
|
||||
private static final int UPLOAD_LIMIT = 100;
|
||||
|
||||
protected Bitmap mBitmap;
|
||||
private int mBorder;
|
||||
|
||||
protected UploadedTexture() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
protected UploadedTexture(boolean hasBorder) {
|
||||
super(null, 0, STATE_UNLOADED);
|
||||
if (hasBorder) {
|
||||
setBorder(true);
|
||||
mBorder = 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected void setIsUploading(boolean uploading) {
|
||||
mIsUploading = uploading;
|
||||
}
|
||||
|
||||
public boolean isUploading() {
|
||||
return mIsUploading;
|
||||
}
|
||||
|
||||
private static class BorderKey implements Cloneable {
|
||||
public boolean vertical;
|
||||
public Config config;
|
||||
public int length;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int x = config.hashCode() ^ length;
|
||||
return vertical ? x : -x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof BorderKey)) return false;
|
||||
BorderKey o = (BorderKey) object;
|
||||
return vertical == o.vertical
|
||||
&& config == o.config && length == o.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BorderKey clone() {
|
||||
try {
|
||||
return (BorderKey) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setThrottled(boolean throttled) {
|
||||
mThrottled = throttled;
|
||||
}
|
||||
|
||||
private static Bitmap getBorderLine(
|
||||
boolean vertical, Config config, int length) {
|
||||
BorderKey key = sBorderKey;
|
||||
key.vertical = vertical;
|
||||
key.config = config;
|
||||
key.length = length;
|
||||
Bitmap bitmap = sBorderLines.get(key);
|
||||
if (bitmap == null) {
|
||||
bitmap = vertical
|
||||
? Bitmap.createBitmap(1, length, config)
|
||||
: Bitmap.createBitmap(length, 1, config);
|
||||
sBorderLines.put(key.clone(), bitmap);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private Bitmap getBitmap() {
|
||||
if (mBitmap == null) {
|
||||
mBitmap = onGetBitmap();
|
||||
int w = mBitmap.getWidth() + mBorder * 2;
|
||||
int h = mBitmap.getHeight() + mBorder * 2;
|
||||
if (mWidth == UNSPECIFIED) {
|
||||
setSize(w, h);
|
||||
}
|
||||
}
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
private void freeBitmap() {
|
||||
Assert.assertTrue(mBitmap != null);
|
||||
onFreeBitmap(mBitmap);
|
||||
mBitmap = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
if (mWidth == UNSPECIFIED) getBitmap();
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
if (mWidth == UNSPECIFIED) getBitmap();
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
protected abstract Bitmap onGetBitmap();
|
||||
|
||||
protected abstract void onFreeBitmap(Bitmap bitmap);
|
||||
|
||||
protected void invalidateContent() {
|
||||
if (mBitmap != null) freeBitmap();
|
||||
mContentValid = false;
|
||||
mWidth = UNSPECIFIED;
|
||||
mHeight = UNSPECIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the content on GPU is valid.
|
||||
*/
|
||||
public boolean isContentValid() {
|
||||
return isLoaded() && mContentValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content on GPU's memory.
|
||||
* @param canvas
|
||||
*/
|
||||
public void updateContent(GLCanvas canvas) {
|
||||
if (!isLoaded()) {
|
||||
if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
|
||||
return;
|
||||
}
|
||||
uploadToCanvas(canvas);
|
||||
} else if (!mContentValid) {
|
||||
Bitmap bitmap = getBitmap();
|
||||
int format = GLUtils.getInternalFormat(bitmap);
|
||||
int type = GLUtils.getType(bitmap);
|
||||
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
|
||||
freeBitmap();
|
||||
mContentValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetUploadLimit() {
|
||||
sUploadedCount = 0;
|
||||
}
|
||||
|
||||
public static boolean uploadLimitReached() {
|
||||
return sUploadedCount > UPLOAD_LIMIT;
|
||||
}
|
||||
|
||||
private void uploadToCanvas(GLCanvas canvas) {
|
||||
|
||||
Bitmap bitmap = getBitmap();
|
||||
if (bitmap != null) {
|
||||
try {
|
||||
int bWidth = bitmap.getWidth();
|
||||
int bHeight = bitmap.getHeight();
|
||||
int width = bWidth + mBorder * 2;
|
||||
int height = bHeight + mBorder * 2;
|
||||
int texWidth = getTextureWidth();
|
||||
int texHeight = getTextureHeight();
|
||||
|
||||
Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
|
||||
|
||||
// Upload the bitmap to a new texture.
|
||||
mId = canvas.getGLId().generateTexture();
|
||||
canvas.setTextureParameters(this);
|
||||
|
||||
if (bWidth == texWidth && bHeight == texHeight) {
|
||||
canvas.initializeTexture(this, bitmap);
|
||||
} else {
|
||||
int format = GLUtils.getInternalFormat(bitmap);
|
||||
int type = GLUtils.getType(bitmap);
|
||||
Config config = bitmap.getConfig();
|
||||
|
||||
canvas.initializeTextureSize(this, format, type);
|
||||
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
|
||||
|
||||
if (mBorder > 0) {
|
||||
// Left border
|
||||
Bitmap line = getBorderLine(true, config, texHeight);
|
||||
canvas.texSubImage2D(this, 0, 0, line, format, type);
|
||||
|
||||
// Top border
|
||||
line = getBorderLine(false, config, texWidth);
|
||||
canvas.texSubImage2D(this, 0, 0, line, format, type);
|
||||
}
|
||||
|
||||
// Right border
|
||||
if (mBorder + bWidth < texWidth) {
|
||||
Bitmap line = getBorderLine(true, config, texHeight);
|
||||
canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
if (mBorder + bHeight < texHeight) {
|
||||
Bitmap line = getBorderLine(false, config, texWidth);
|
||||
canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
freeBitmap();
|
||||
}
|
||||
// Update texture state.
|
||||
setAssociatedCanvas(canvas);
|
||||
mState = STATE_LOADED;
|
||||
mContentValid = true;
|
||||
} else {
|
||||
mState = STATE_ERROR;
|
||||
throw new RuntimeException("Texture load fail, no bitmap");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onBind(GLCanvas canvas) {
|
||||
updateContent(canvas);
|
||||
return isContentValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTarget() {
|
||||
return GL11.GL_TEXTURE_2D;
|
||||
}
|
||||
|
||||
public void setOpaque(boolean isOpaque) {
|
||||
mOpaque = isOpaque;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaque() {
|
||||
return mOpaque;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle() {
|
||||
super.recycle();
|
||||
if (mBitmap != null) freeBitmap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.
|
||||
*/
|
||||
|
||||
package com.android.gallery3d.util;
|
||||
|
||||
public class IntArray {
|
||||
private static final int INIT_CAPACITY = 8;
|
||||
|
||||
private int mData[] = new int[INIT_CAPACITY];
|
||||
private int mSize = 0;
|
||||
|
||||
public void add(int value) {
|
||||
if (mData.length == mSize) {
|
||||
int temp[] = new int[mSize + mSize];
|
||||
System.arraycopy(mData, 0, temp, 0, mSize);
|
||||
mData = temp;
|
||||
}
|
||||
mData[mSize++] = value;
|
||||
}
|
||||
|
||||
public int removeLast() {
|
||||
mSize--;
|
||||
return mData[mSize];
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
// For testing only
|
||||
public int[] toArray(int[] result) {
|
||||
if (result == null || result.length < mSize) {
|
||||
result = new int[mSize];
|
||||
}
|
||||
System.arraycopy(mData, 0, result, 0, mSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] getInternalArray() {
|
||||
return mData;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mSize = 0;
|
||||
if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
|
||||
package com.android.photos;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.gallery3d.common.BitmapUtils;
|
||||
import com.android.gallery3d.glrenderer.BasicTexture;
|
||||
import com.android.gallery3d.glrenderer.BitmapTexture;
|
||||
import com.android.photos.views.TiledImageRenderer;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
|
||||
* {@link BitmapRegionDecoder} to wrap a local file
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
|
||||
public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
|
||||
|
||||
private static final String TAG = "BitmapRegionTileSource";
|
||||
|
||||
private static final boolean REUSE_BITMAP =
|
||||
Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
|
||||
private static final int GL_SIZE_LIMIT = 2048;
|
||||
// This must be no larger than half the size of the GL_SIZE_LIMIT
|
||||
// due to decodePreview being allowed to be up to 2x the size of the target
|
||||
private static final int MAX_PREVIEW_SIZE = 1024;
|
||||
|
||||
BitmapRegionDecoder mDecoder;
|
||||
int mWidth;
|
||||
int mHeight;
|
||||
int mTileSize;
|
||||
private BasicTexture mPreview;
|
||||
private final int mRotation;
|
||||
|
||||
// For use only by getTile
|
||||
private Rect mWantRegion = new Rect();
|
||||
private Rect mOverlapRegion = new Rect();
|
||||
private BitmapFactory.Options mOptions;
|
||||
private Canvas mCanvas;
|
||||
|
||||
public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
|
||||
this(null, context, path, null, 0, previewSize, rotation);
|
||||
}
|
||||
|
||||
public BitmapRegionTileSource(Context context, Uri uri, int previewSize, int rotation) {
|
||||
this(null, context, null, uri, 0, previewSize, rotation);
|
||||
}
|
||||
|
||||
public BitmapRegionTileSource(Resources res,
|
||||
Context context, int resId, int previewSize, int rotation) {
|
||||
this(res, context, null, null, resId, previewSize, rotation);
|
||||
}
|
||||
|
||||
private BitmapRegionTileSource(Resources res,
|
||||
Context context, String path, Uri uri, int resId, int previewSize, int rotation) {
|
||||
mTileSize = TiledImageRenderer.suggestedTileSize(context);
|
||||
mRotation = rotation;
|
||||
try {
|
||||
if (path != null) {
|
||||
mDecoder = BitmapRegionDecoder.newInstance(path, true);
|
||||
} else if (uri != null) {
|
||||
InputStream is = context.getContentResolver().openInputStream(uri);
|
||||
BufferedInputStream bis = new BufferedInputStream(is);
|
||||
mDecoder = BitmapRegionDecoder.newInstance(bis, true);
|
||||
} else {
|
||||
InputStream is = res.openRawResource(resId);
|
||||
BufferedInputStream bis = new BufferedInputStream(is);
|
||||
mDecoder = BitmapRegionDecoder.newInstance(bis, true);
|
||||
}
|
||||
mWidth = mDecoder.getWidth();
|
||||
mHeight = mDecoder.getHeight();
|
||||
} catch (IOException e) {
|
||||
Log.w("BitmapRegionTileSource", "ctor failed", e);
|
||||
}
|
||||
mOptions = new BitmapFactory.Options();
|
||||
mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
|
||||
mOptions.inPreferQualityOverSpeed = true;
|
||||
mOptions.inTempStorage = new byte[16 * 1024];
|
||||
if (previewSize != 0) {
|
||||
previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
|
||||
// Although this is the same size as the Bitmap that is likely already
|
||||
// loaded, the lifecycle is different and interactions are on a different
|
||||
// thread. Thus to simplify, this source will decode its own bitmap.
|
||||
Bitmap preview = decodePreview(res, context, path, uri, resId, previewSize);
|
||||
if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
|
||||
mPreview = new BitmapTexture(preview);
|
||||
} else {
|
||||
Log.w(TAG, String.format(
|
||||
"Failed to create preview of apropriate size! "
|
||||
+ " in: %dx%d, out: %dx%d",
|
||||
mWidth, mHeight,
|
||||
preview.getWidth(), preview.getHeight()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTileSize() {
|
||||
return mTileSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicTexture getPreview() {
|
||||
return mPreview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRotation() {
|
||||
return mRotation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
|
||||
int tileSize = getTileSize();
|
||||
if (!REUSE_BITMAP) {
|
||||
return getTileWithoutReusingBitmap(level, x, y, tileSize);
|
||||
}
|
||||
|
||||
int t = tileSize << level;
|
||||
mWantRegion.set(x, y, x + t, y + t);
|
||||
|
||||
if (bitmap == null) {
|
||||
bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
mOptions.inSampleSize = (1 << level);
|
||||
mOptions.inBitmap = bitmap;
|
||||
|
||||
try {
|
||||
bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
|
||||
} finally {
|
||||
if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
|
||||
mOptions.inBitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
Log.w("BitmapRegionTileSource", "fail in decoding region");
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private Bitmap getTileWithoutReusingBitmap(
|
||||
int level, int x, int y, int tileSize) {
|
||||
|
||||
int t = tileSize << level;
|
||||
mWantRegion.set(x, y, x + t, y + t);
|
||||
|
||||
mOverlapRegion.set(0, 0, mWidth, mHeight);
|
||||
|
||||
mOptions.inSampleSize = (1 << level);
|
||||
Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
|
||||
|
||||
if (bitmap == null) {
|
||||
Log.w(TAG, "fail in decoding region");
|
||||
}
|
||||
|
||||
if (mWantRegion.equals(mOverlapRegion)) {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
|
||||
if (mCanvas == null) {
|
||||
mCanvas = new Canvas();
|
||||
}
|
||||
mCanvas.setBitmap(result);
|
||||
mCanvas.drawBitmap(bitmap,
|
||||
(mOverlapRegion.left - mWantRegion.left) >> level,
|
||||
(mOverlapRegion.top - mWantRegion.top) >> level, null);
|
||||
mCanvas.setBitmap(null);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that the returned bitmap may have a long edge that's longer
|
||||
* than the targetSize, but it will always be less than 2x the targetSize
|
||||
*/
|
||||
private Bitmap decodePreview(
|
||||
Resources res, Context context, String file, Uri uri, int resId, int targetSize) {
|
||||
float scale = (float) targetSize / Math.max(mWidth, mHeight);
|
||||
mOptions.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
|
||||
mOptions.inJustDecodeBounds = false;
|
||||
|
||||
Bitmap result = null;
|
||||
if (file != null) {
|
||||
result = BitmapFactory.decodeFile(file, mOptions);
|
||||
} else if (uri != null) {
|
||||
try {
|
||||
InputStream is = context.getContentResolver().openInputStream(uri);
|
||||
BufferedInputStream bis = new BufferedInputStream(is);
|
||||
result = BitmapFactory.decodeStream(bis, null, mOptions);
|
||||
} catch (IOException e) {
|
||||
Log.w("BitmapRegionTileSource", "getting preview failed", e);
|
||||
}
|
||||
} else {
|
||||
result = BitmapFactory.decodeResource(res, resId, mOptions);
|
||||
}
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We need to resize down if the decoder does not support inSampleSize
|
||||
// or didn't support the specified inSampleSize (some decoders only do powers of 2)
|
||||
scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
|
||||
|
||||
if (scale <= 0.5) {
|
||||
result = BitmapUtils.resizeBitmapByScale(result, scale, true);
|
||||
}
|
||||
return ensureGLCompatibleBitmap(result);
|
||||
}
|
||||
|
||||
private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
|
||||
if (bitmap == null || bitmap.getConfig() != null) {
|
||||
return bitmap;
|
||||
}
|
||||
Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
|
||||
bitmap.recycle();
|
||||
return newBitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
|
||||
package com.android.photos.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.GLSurfaceView.Renderer;
|
||||
import android.opengl.GLUtils;
|
||||
import android.util.Log;
|
||||
import android.view.TextureView;
|
||||
import android.view.TextureView.SurfaceTextureListener;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* A TextureView that supports blocking rendering for synchronous drawing
|
||||
*/
|
||||
public class BlockingGLTextureView extends TextureView
|
||||
implements SurfaceTextureListener {
|
||||
|
||||
private RenderThread mRenderThread;
|
||||
|
||||
public BlockingGLTextureView(Context context) {
|
||||
super(context);
|
||||
setSurfaceTextureListener(this);
|
||||
}
|
||||
|
||||
public void setRenderer(Renderer renderer) {
|
||||
if (mRenderThread != null) {
|
||||
throw new IllegalArgumentException("Renderer already set");
|
||||
}
|
||||
mRenderThread = new RenderThread(renderer);
|
||||
}
|
||||
|
||||
public void render() {
|
||||
mRenderThread.render();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (mRenderThread != null) {
|
||||
mRenderThread.finish();
|
||||
mRenderThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
|
||||
int height) {
|
||||
mRenderThread.setSurface(surface);
|
||||
mRenderThread.setSize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
|
||||
int height) {
|
||||
mRenderThread.setSize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||
if (mRenderThread != null) {
|
||||
mRenderThread.setSurface(null);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
try {
|
||||
destroy();
|
||||
} catch (Throwable t) {
|
||||
// Ignore
|
||||
}
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* An EGL helper class.
|
||||
*/
|
||||
|
||||
private static class EglHelper {
|
||||
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
private static final int EGL_OPENGL_ES2_BIT = 4;
|
||||
|
||||
EGL10 mEgl;
|
||||
EGLDisplay mEglDisplay;
|
||||
EGLSurface mEglSurface;
|
||||
EGLConfig mEglConfig;
|
||||
EGLContext mEglContext;
|
||||
|
||||
private EGLConfig chooseEglConfig() {
|
||||
int[] configsCount = new int[1];
|
||||
EGLConfig[] configs = new EGLConfig[1];
|
||||
int[] configSpec = getConfig();
|
||||
if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
|
||||
throw new IllegalArgumentException("eglChooseConfig failed " +
|
||||
GLUtils.getEGLErrorString(mEgl.eglGetError()));
|
||||
} else if (configsCount[0] > 0) {
|
||||
return configs[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int[] getConfig() {
|
||||
return new int[] {
|
||||
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL10.EGL_RED_SIZE, 8,
|
||||
EGL10.EGL_GREEN_SIZE, 8,
|
||||
EGL10.EGL_BLUE_SIZE, 8,
|
||||
EGL10.EGL_ALPHA_SIZE, 8,
|
||||
EGL10.EGL_DEPTH_SIZE, 0,
|
||||
EGL10.EGL_STENCIL_SIZE, 0,
|
||||
EGL10.EGL_NONE
|
||||
};
|
||||
}
|
||||
|
||||
EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
|
||||
int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
|
||||
return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize EGL for a given configuration spec.
|
||||
*/
|
||||
public void start() {
|
||||
/*
|
||||
* Get an EGL instance
|
||||
*/
|
||||
mEgl = (EGL10) EGLContext.getEGL();
|
||||
|
||||
/*
|
||||
* Get to the default display.
|
||||
*/
|
||||
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
|
||||
throw new RuntimeException("eglGetDisplay failed");
|
||||
}
|
||||
|
||||
/*
|
||||
* We can now initialize EGL for that display
|
||||
*/
|
||||
int[] version = new int[2];
|
||||
if (!mEgl.eglInitialize(mEglDisplay, version)) {
|
||||
throw new RuntimeException("eglInitialize failed");
|
||||
}
|
||||
mEglConfig = chooseEglConfig();
|
||||
|
||||
/*
|
||||
* Create an EGL context. We want to do this as rarely as we can, because an
|
||||
* EGL context is a somewhat heavy object.
|
||||
*/
|
||||
mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
|
||||
|
||||
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
|
||||
mEglContext = null;
|
||||
throwEglException("createContext");
|
||||
}
|
||||
|
||||
mEglSurface = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an egl surface for the current SurfaceTexture surface. If a surface
|
||||
* already exists, destroy it before creating the new surface.
|
||||
*
|
||||
* @return true if the surface was created successfully.
|
||||
*/
|
||||
public boolean createSurface(SurfaceTexture surface) {
|
||||
/*
|
||||
* Check preconditions.
|
||||
*/
|
||||
if (mEgl == null) {
|
||||
throw new RuntimeException("egl not initialized");
|
||||
}
|
||||
if (mEglDisplay == null) {
|
||||
throw new RuntimeException("eglDisplay not initialized");
|
||||
}
|
||||
if (mEglConfig == null) {
|
||||
throw new RuntimeException("mEglConfig not initialized");
|
||||
}
|
||||
|
||||
/*
|
||||
* The window size has changed, so we need to create a new
|
||||
* surface.
|
||||
*/
|
||||
destroySurfaceImp();
|
||||
|
||||
/*
|
||||
* Create an EGL surface we can render into.
|
||||
*/
|
||||
if (surface != null) {
|
||||
mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
|
||||
} else {
|
||||
mEglSurface = null;
|
||||
}
|
||||
|
||||
if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
|
||||
int error = mEgl.eglGetError();
|
||||
if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
|
||||
Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Before we can issue GL commands, we need to make sure
|
||||
* the context is current and bound to a surface.
|
||||
*/
|
||||
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
|
||||
/*
|
||||
* Could not make the context current, probably because the underlying
|
||||
* SurfaceView surface has been destroyed.
|
||||
*/
|
||||
logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GL object for the current EGL context.
|
||||
*/
|
||||
public GL10 createGL() {
|
||||
return (GL10) mEglContext.getGL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the current render surface.
|
||||
* @return the EGL error code from eglSwapBuffers.
|
||||
*/
|
||||
public int swap() {
|
||||
if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
|
||||
return mEgl.eglGetError();
|
||||
}
|
||||
return EGL10.EGL_SUCCESS;
|
||||
}
|
||||
|
||||
public void destroySurface() {
|
||||
destroySurfaceImp();
|
||||
}
|
||||
|
||||
private void destroySurfaceImp() {
|
||||
if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
|
||||
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_CONTEXT);
|
||||
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
|
||||
mEglSurface = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
if (mEglContext != null) {
|
||||
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
|
||||
mEglContext = null;
|
||||
}
|
||||
if (mEglDisplay != null) {
|
||||
mEgl.eglTerminate(mEglDisplay);
|
||||
mEglDisplay = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void throwEglException(String function) {
|
||||
throwEglException(function, mEgl.eglGetError());
|
||||
}
|
||||
|
||||
public static void throwEglException(String function, int error) {
|
||||
String message = formatEglError(function, error);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
public static void logEglErrorAsWarning(String tag, String function, int error) {
|
||||
Log.w(tag, formatEglError(function, error));
|
||||
}
|
||||
|
||||
public static String formatEglError(String function, int error) {
|
||||
return function + " failed: " + error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class RenderThread extends Thread {
|
||||
private static final int INVALID = -1;
|
||||
private static final int RENDER = 1;
|
||||
private static final int CHANGE_SURFACE = 2;
|
||||
private static final int RESIZE_SURFACE = 3;
|
||||
private static final int FINISH = 4;
|
||||
|
||||
private EglHelper mEglHelper = new EglHelper();
|
||||
|
||||
private Object mLock = new Object();
|
||||
private int mExecMsgId = INVALID;
|
||||
private SurfaceTexture mSurface;
|
||||
private Renderer mRenderer;
|
||||
private int mWidth, mHeight;
|
||||
|
||||
private boolean mFinished = false;
|
||||
private GL10 mGL;
|
||||
|
||||
public RenderThread(Renderer renderer) {
|
||||
super("RenderThread");
|
||||
mRenderer = renderer;
|
||||
start();
|
||||
}
|
||||
|
||||
private void checkRenderer() {
|
||||
if (mRenderer == null) {
|
||||
throw new IllegalArgumentException("Renderer is null!");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSurface() {
|
||||
if (mSurface == null) {
|
||||
throw new IllegalArgumentException("surface is null!");
|
||||
}
|
||||
}
|
||||
|
||||
public void setSurface(SurfaceTexture surface) {
|
||||
// If the surface is null we're being torn down, don't need a
|
||||
// renderer then
|
||||
if (surface != null) {
|
||||
checkRenderer();
|
||||
}
|
||||
mSurface = surface;
|
||||
exec(CHANGE_SURFACE);
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
checkRenderer();
|
||||
checkSurface();
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
exec(RESIZE_SURFACE);
|
||||
}
|
||||
|
||||
public void render() {
|
||||
checkRenderer();
|
||||
if (mSurface != null) {
|
||||
exec(RENDER);
|
||||
mSurface.updateTexImage();
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
mSurface = null;
|
||||
exec(FINISH);
|
||||
try {
|
||||
join();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void exec(int msgid) {
|
||||
synchronized (mLock) {
|
||||
if (mExecMsgId != INVALID) {
|
||||
throw new IllegalArgumentException(
|
||||
"Message already set - multithreaded access?");
|
||||
}
|
||||
mExecMsgId = msgid;
|
||||
mLock.notify();
|
||||
try {
|
||||
mLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessageLocked(int what) {
|
||||
switch (what) {
|
||||
case CHANGE_SURFACE:
|
||||
if (mEglHelper.createSurface(mSurface)) {
|
||||
mGL = mEglHelper.createGL();
|
||||
mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
|
||||
}
|
||||
break;
|
||||
case RESIZE_SURFACE:
|
||||
mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
|
||||
break;
|
||||
case RENDER:
|
||||
mRenderer.onDrawFrame(mGL);
|
||||
mEglHelper.swap();
|
||||
break;
|
||||
case FINISH:
|
||||
mEglHelper.destroySurface();
|
||||
mEglHelper.finish();
|
||||
mFinished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mLock) {
|
||||
mEglHelper.start();
|
||||
while (!mFinished) {
|
||||
while (mExecMsgId == INVALID) {
|
||||
try {
|
||||
mLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
handleMessageLocked(mExecMsgId);
|
||||
mExecMsgId = INVALID;
|
||||
mLock.notify();
|
||||
}
|
||||
mExecMsgId = FINISH;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,825 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
|
||||
package com.android.photos.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.Pools.Pool;
|
||||
import android.util.Pools.SynchronizedPool;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.gallery3d.common.Utils;
|
||||
import com.android.gallery3d.glrenderer.BasicTexture;
|
||||
import com.android.gallery3d.glrenderer.GLCanvas;
|
||||
import com.android.gallery3d.glrenderer.UploadedTexture;
|
||||
|
||||
/**
|
||||
* Handles laying out, decoding, and drawing of tiles in GL
|
||||
*/
|
||||
public class TiledImageRenderer {
|
||||
public static final int SIZE_UNKNOWN = -1;
|
||||
|
||||
private static final String TAG = "TiledImageRenderer";
|
||||
private static final int UPLOAD_LIMIT = 1;
|
||||
|
||||
/*
|
||||
* This is the tile state in the CPU side.
|
||||
* Life of a Tile:
|
||||
* ACTIVATED (initial state)
|
||||
* --> IN_QUEUE - by queueForDecode()
|
||||
* --> RECYCLED - by recycleTile()
|
||||
* IN_QUEUE --> DECODING - by decodeTile()
|
||||
* --> RECYCLED - by recycleTile)
|
||||
* DECODING --> RECYCLING - by recycleTile()
|
||||
* --> DECODED - by decodeTile()
|
||||
* --> DECODE_FAIL - by decodeTile()
|
||||
* RECYCLING --> RECYCLED - by decodeTile()
|
||||
* DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
|
||||
* DECODED --> RECYCLED - by recycleTile()
|
||||
* DECODE_FAIL -> RECYCLED - by recycleTile()
|
||||
* RECYCLED --> ACTIVATED - by obtainTile()
|
||||
*/
|
||||
private static final int STATE_ACTIVATED = 0x01;
|
||||
private static final int STATE_IN_QUEUE = 0x02;
|
||||
private static final int STATE_DECODING = 0x04;
|
||||
private static final int STATE_DECODED = 0x08;
|
||||
private static final int STATE_DECODE_FAIL = 0x10;
|
||||
private static final int STATE_RECYCLING = 0x20;
|
||||
private static final int STATE_RECYCLED = 0x40;
|
||||
|
||||
private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
|
||||
|
||||
// TILE_SIZE must be 2^N
|
||||
private int mTileSize;
|
||||
|
||||
private TileSource mModel;
|
||||
private BasicTexture mPreview;
|
||||
protected int mLevelCount; // cache the value of mScaledBitmaps.length
|
||||
|
||||
// The mLevel variable indicates which level of bitmap we should use.
|
||||
// Level 0 means the original full-sized bitmap, and a larger value means
|
||||
// a smaller scaled bitmap (The width and height of each scaled bitmap is
|
||||
// half size of the previous one). If the value is in [0, mLevelCount), we
|
||||
// use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
|
||||
// is mLevelCount
|
||||
private int mLevel = 0;
|
||||
|
||||
private int mOffsetX;
|
||||
private int mOffsetY;
|
||||
|
||||
private int mUploadQuota;
|
||||
private boolean mRenderComplete;
|
||||
|
||||
private final RectF mSourceRect = new RectF();
|
||||
private final RectF mTargetRect = new RectF();
|
||||
|
||||
private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
|
||||
|
||||
// The following three queue are guarded by mQueueLock
|
||||
private final Object mQueueLock = new Object();
|
||||
private final TileQueue mRecycledQueue = new TileQueue();
|
||||
private final TileQueue mUploadQueue = new TileQueue();
|
||||
private final TileQueue mDecodeQueue = new TileQueue();
|
||||
|
||||
// The width and height of the full-sized bitmap
|
||||
protected int mImageWidth = SIZE_UNKNOWN;
|
||||
protected int mImageHeight = SIZE_UNKNOWN;
|
||||
|
||||
protected int mCenterX;
|
||||
protected int mCenterY;
|
||||
protected float mScale;
|
||||
protected int mRotation;
|
||||
|
||||
private boolean mLayoutTiles;
|
||||
|
||||
// Temp variables to avoid memory allocation
|
||||
private final Rect mTileRange = new Rect();
|
||||
private final Rect mActiveRange[] = {new Rect(), new Rect()};
|
||||
|
||||
private TileDecoder mTileDecoder;
|
||||
private boolean mBackgroundTileUploaded;
|
||||
|
||||
private int mViewWidth, mViewHeight;
|
||||
private View mParent;
|
||||
|
||||
/**
|
||||
* Interface for providing tiles to a {@link TiledImageRenderer}
|
||||
*/
|
||||
public static interface TileSource {
|
||||
|
||||
/**
|
||||
* If the source does not care about the tile size, it should use
|
||||
* {@link TiledImageRenderer#suggestedTileSize(Context)}
|
||||
*/
|
||||
public int getTileSize();
|
||||
public int getImageWidth();
|
||||
public int getImageHeight();
|
||||
public int getRotation();
|
||||
|
||||
/**
|
||||
* Return a Preview image if available. This will be used as the base layer
|
||||
* if higher res tiles are not yet available
|
||||
*/
|
||||
public BasicTexture getPreview();
|
||||
|
||||
/**
|
||||
* The tile returned by this method can be specified this way: Assuming
|
||||
* the image size is (width, height), first take the intersection of (0,
|
||||
* 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
|
||||
* in extending the region, we found some part of the region is outside
|
||||
* the image, those pixels are filled with black.
|
||||
*
|
||||
* If level > 0, it does the same operation on a down-scaled version of
|
||||
* the original image (down-scaled by a factor of 2^level), but (x, y)
|
||||
* still refers to the coordinate on the original image.
|
||||
*
|
||||
* The method would be called by the decoder thread.
|
||||
*/
|
||||
public Bitmap getTile(int level, int x, int y, Bitmap reuse);
|
||||
}
|
||||
|
||||
public static int suggestedTileSize(Context context) {
|
||||
return isHighResolution(context) ? 512 : 256;
|
||||
}
|
||||
|
||||
private static boolean isHighResolution(Context context) {
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
WindowManager wm = (WindowManager)
|
||||
context.getSystemService(Context.WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getMetrics(metrics);
|
||||
return metrics.heightPixels > 2048 || metrics.widthPixels > 2048;
|
||||
}
|
||||
|
||||
public TiledImageRenderer(View parent) {
|
||||
mParent = parent;
|
||||
mTileDecoder = new TileDecoder();
|
||||
mTileDecoder.start();
|
||||
}
|
||||
|
||||
public int getViewWidth() {
|
||||
return mViewWidth;
|
||||
}
|
||||
|
||||
public int getViewHeight() {
|
||||
return mViewHeight;
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
mParent.postInvalidate();
|
||||
}
|
||||
|
||||
public void setModel(TileSource model, int rotation) {
|
||||
if (mModel != model) {
|
||||
mModel = model;
|
||||
notifyModelInvalidated();
|
||||
}
|
||||
if (mRotation != rotation) {
|
||||
mRotation = rotation;
|
||||
mLayoutTiles = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateLevelCount() {
|
||||
if (mPreview != null) {
|
||||
mLevelCount = Math.max(0, Utils.ceilLog2(
|
||||
mImageWidth / (float) mPreview.getWidth()));
|
||||
} else {
|
||||
int levels = 1;
|
||||
int maxDim = Math.max(mImageWidth, mImageHeight);
|
||||
int t = mTileSize;
|
||||
while (t < maxDim) {
|
||||
t <<= 1;
|
||||
levels++;
|
||||
}
|
||||
mLevelCount = levels;
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyModelInvalidated() {
|
||||
invalidateTiles();
|
||||
if (mModel == null) {
|
||||
mImageWidth = 0;
|
||||
mImageHeight = 0;
|
||||
mLevelCount = 0;
|
||||
mPreview = null;
|
||||
} else {
|
||||
mImageWidth = mModel.getImageWidth();
|
||||
mImageHeight = mModel.getImageHeight();
|
||||
mPreview = mModel.getPreview();
|
||||
mTileSize = mModel.getTileSize();
|
||||
calculateLevelCount();
|
||||
}
|
||||
mLayoutTiles = true;
|
||||
}
|
||||
|
||||
public void setViewSize(int width, int height) {
|
||||
mViewWidth = width;
|
||||
mViewHeight = height;
|
||||
}
|
||||
|
||||
public void setPosition(int centerX, int centerY, float scale) {
|
||||
if (mCenterX == centerX && mCenterY == centerY
|
||||
&& mScale == scale) {
|
||||
return;
|
||||
}
|
||||
mCenterX = centerX;
|
||||
mCenterY = centerY;
|
||||
mScale = scale;
|
||||
mLayoutTiles = true;
|
||||
}
|
||||
|
||||
// Prepare the tiles we want to use for display.
|
||||
//
|
||||
// 1. Decide the tile level we want to use for display.
|
||||
// 2. Decide the tile levels we want to keep as texture (in addition to
|
||||
// the one we use for display).
|
||||
// 3. Recycle unused tiles.
|
||||
// 4. Activate the tiles we want.
|
||||
private void layoutTiles() {
|
||||
if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) {
|
||||
return;
|
||||
}
|
||||
mLayoutTiles = false;
|
||||
|
||||
// The tile levels we want to keep as texture is in the range
|
||||
// [fromLevel, endLevel).
|
||||
int fromLevel;
|
||||
int endLevel;
|
||||
|
||||
// We want to use a texture larger than or equal to the display size.
|
||||
mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount);
|
||||
|
||||
// We want to keep one more tile level as texture in addition to what
|
||||
// we use for display. So it can be faster when the scale moves to the
|
||||
// next level. We choose the level closest to the current scale.
|
||||
if (mLevel != mLevelCount) {
|
||||
Rect range = mTileRange;
|
||||
getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation);
|
||||
mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale);
|
||||
mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale);
|
||||
fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
|
||||
} else {
|
||||
// Activate the tiles of the smallest two levels.
|
||||
fromLevel = mLevel - 2;
|
||||
mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale);
|
||||
mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale);
|
||||
}
|
||||
|
||||
fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
|
||||
endLevel = Math.min(fromLevel + 2, mLevelCount);
|
||||
|
||||
Rect range[] = mActiveRange;
|
||||
for (int i = fromLevel; i < endLevel; ++i) {
|
||||
getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation);
|
||||
}
|
||||
|
||||
// If rotation is transient, don't update the tile.
|
||||
if (mRotation % 90 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mQueueLock) {
|
||||
mDecodeQueue.clean();
|
||||
mUploadQueue.clean();
|
||||
mBackgroundTileUploaded = false;
|
||||
|
||||
// Recycle unused tiles: if the level of the active tile is outside the
|
||||
// range [fromLevel, endLevel) or not in the visible range.
|
||||
int n = mActiveTiles.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Tile tile = mActiveTiles.valueAt(i);
|
||||
int level = tile.mTileLevel;
|
||||
if (level < fromLevel || level >= endLevel
|
||||
|| !range[level - fromLevel].contains(tile.mX, tile.mY)) {
|
||||
mActiveTiles.removeAt(i);
|
||||
i--;
|
||||
n--;
|
||||
recycleTile(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = fromLevel; i < endLevel; ++i) {
|
||||
int size = mTileSize << i;
|
||||
Rect r = range[i - fromLevel];
|
||||
for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
|
||||
for (int x = r.left, right = r.right; x < right; x += size) {
|
||||
activateTile(x, y, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void invalidateTiles() {
|
||||
synchronized (mQueueLock) {
|
||||
mDecodeQueue.clean();
|
||||
mUploadQueue.clean();
|
||||
|
||||
// TODO(xx): disable decoder
|
||||
int n = mActiveTiles.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Tile tile = mActiveTiles.valueAt(i);
|
||||
recycleTile(tile);
|
||||
}
|
||||
mActiveTiles.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void getRange(Rect out, int cX, int cY, int level, int rotation) {
|
||||
getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
|
||||
}
|
||||
|
||||
// If the bitmap is scaled by the given factor "scale", return the
|
||||
// rectangle containing visible range. The left-top coordinate returned is
|
||||
// aligned to the tile boundary.
|
||||
//
|
||||
// (cX, cY) is the point on the original bitmap which will be put in the
|
||||
// center of the ImageViewer.
|
||||
private void getRange(Rect out,
|
||||
int cX, int cY, int level, float scale, int rotation) {
|
||||
|
||||
double radians = Math.toRadians(-rotation);
|
||||
double w = mViewWidth;
|
||||
double h = mViewHeight;
|
||||
|
||||
double cos = Math.cos(radians);
|
||||
double sin = Math.sin(radians);
|
||||
int width = (int) Math.ceil(Math.max(
|
||||
Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
|
||||
int height = (int) Math.ceil(Math.max(
|
||||
Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
|
||||
|
||||
int left = (int) Math.floor(cX - width / (2f * scale));
|
||||
int top = (int) Math.floor(cY - height / (2f * scale));
|
||||
int right = (int) Math.ceil(left + width / scale);
|
||||
int bottom = (int) Math.ceil(top + height / scale);
|
||||
|
||||
// align the rectangle to tile boundary
|
||||
int size = mTileSize << level;
|
||||
left = Math.max(0, size * (left / size));
|
||||
top = Math.max(0, size * (top / size));
|
||||
right = Math.min(mImageWidth, right);
|
||||
bottom = Math.min(mImageHeight, bottom);
|
||||
|
||||
out.set(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public void freeTextures() {
|
||||
mLayoutTiles = true;
|
||||
|
||||
mTileDecoder.finishAndWait();
|
||||
synchronized (mQueueLock) {
|
||||
mUploadQueue.clean();
|
||||
mDecodeQueue.clean();
|
||||
Tile tile = mRecycledQueue.pop();
|
||||
while (tile != null) {
|
||||
tile.recycle();
|
||||
tile = mRecycledQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
int n = mActiveTiles.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Tile texture = mActiveTiles.valueAt(i);
|
||||
texture.recycle();
|
||||
}
|
||||
mActiveTiles.clear();
|
||||
mTileRange.set(0, 0, 0, 0);
|
||||
|
||||
while (sTilePool.acquire() != null) {}
|
||||
}
|
||||
|
||||
public boolean draw(GLCanvas canvas) {
|
||||
layoutTiles();
|
||||
uploadTiles(canvas);
|
||||
|
||||
mUploadQuota = UPLOAD_LIMIT;
|
||||
mRenderComplete = true;
|
||||
|
||||
int level = mLevel;
|
||||
int rotation = mRotation;
|
||||
int flags = 0;
|
||||
if (rotation != 0) {
|
||||
flags |= GLCanvas.SAVE_FLAG_MATRIX;
|
||||
}
|
||||
|
||||
if (flags != 0) {
|
||||
canvas.save(flags);
|
||||
if (rotation != 0) {
|
||||
int centerX = mViewWidth / 2, centerY = mViewHeight / 2;
|
||||
canvas.translate(centerX, centerY);
|
||||
canvas.rotate(rotation, 0, 0, 1);
|
||||
canvas.translate(-centerX, -centerY);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (level != mLevelCount) {
|
||||
int size = (mTileSize << level);
|
||||
float length = size * mScale;
|
||||
Rect r = mTileRange;
|
||||
|
||||
for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
|
||||
float y = mOffsetY + i * length;
|
||||
for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
|
||||
float x = mOffsetX + j * length;
|
||||
drawTile(canvas, tx, ty, level, x, y, length);
|
||||
}
|
||||
}
|
||||
} else if (mPreview != null) {
|
||||
mPreview.draw(canvas, mOffsetX, mOffsetY,
|
||||
Math.round(mImageWidth * mScale),
|
||||
Math.round(mImageHeight * mScale));
|
||||
}
|
||||
} finally {
|
||||
if (flags != 0) {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
if (mRenderComplete) {
|
||||
if (!mBackgroundTileUploaded) {
|
||||
uploadBackgroundTiles(canvas);
|
||||
}
|
||||
} else {
|
||||
invalidate();
|
||||
}
|
||||
return mRenderComplete || mPreview != null;
|
||||
}
|
||||
|
||||
private void uploadBackgroundTiles(GLCanvas canvas) {
|
||||
mBackgroundTileUploaded = true;
|
||||
int n = mActiveTiles.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Tile tile = mActiveTiles.valueAt(i);
|
||||
if (!tile.isContentValid()) {
|
||||
queueForDecode(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void queueForDecode(Tile tile) {
|
||||
synchronized (mQueueLock) {
|
||||
if (tile.mTileState == STATE_ACTIVATED) {
|
||||
tile.mTileState = STATE_IN_QUEUE;
|
||||
if (mDecodeQueue.push(tile)) {
|
||||
mQueueLock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeTile(Tile tile) {
|
||||
synchronized (mQueueLock) {
|
||||
if (tile.mTileState != STATE_IN_QUEUE) {
|
||||
return;
|
||||
}
|
||||
tile.mTileState = STATE_DECODING;
|
||||
}
|
||||
boolean decodeComplete = tile.decode();
|
||||
synchronized (mQueueLock) {
|
||||
if (tile.mTileState == STATE_RECYCLING) {
|
||||
tile.mTileState = STATE_RECYCLED;
|
||||
if (tile.mDecodedTile != null) {
|
||||
sTilePool.release(tile.mDecodedTile);
|
||||
tile.mDecodedTile = null;
|
||||
}
|
||||
mRecycledQueue.push(tile);
|
||||
return;
|
||||
}
|
||||
tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
|
||||
if (!decodeComplete) {
|
||||
return;
|
||||
}
|
||||
mUploadQueue.push(tile);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private Tile obtainTile(int x, int y, int level) {
|
||||
synchronized (mQueueLock) {
|
||||
Tile tile = mRecycledQueue.pop();
|
||||
if (tile != null) {
|
||||
tile.mTileState = STATE_ACTIVATED;
|
||||
tile.update(x, y, level);
|
||||
return tile;
|
||||
}
|
||||
return new Tile(x, y, level);
|
||||
}
|
||||
}
|
||||
|
||||
private void recycleTile(Tile tile) {
|
||||
synchronized (mQueueLock) {
|
||||
if (tile.mTileState == STATE_DECODING) {
|
||||
tile.mTileState = STATE_RECYCLING;
|
||||
return;
|
||||
}
|
||||
tile.mTileState = STATE_RECYCLED;
|
||||
if (tile.mDecodedTile != null) {
|
||||
sTilePool.release(tile.mDecodedTile);
|
||||
tile.mDecodedTile = null;
|
||||
}
|
||||
mRecycledQueue.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
private void activateTile(int x, int y, int level) {
|
||||
long key = makeTileKey(x, y, level);
|
||||
Tile tile = mActiveTiles.get(key);
|
||||
if (tile != null) {
|
||||
if (tile.mTileState == STATE_IN_QUEUE) {
|
||||
tile.mTileState = STATE_ACTIVATED;
|
||||
}
|
||||
return;
|
||||
}
|
||||
tile = obtainTile(x, y, level);
|
||||
mActiveTiles.put(key, tile);
|
||||
}
|
||||
|
||||
private Tile getTile(int x, int y, int level) {
|
||||
return mActiveTiles.get(makeTileKey(x, y, level));
|
||||
}
|
||||
|
||||
private static long makeTileKey(int x, int y, int level) {
|
||||
long result = x;
|
||||
result = (result << 16) | y;
|
||||
result = (result << 16) | level;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void uploadTiles(GLCanvas canvas) {
|
||||
int quota = UPLOAD_LIMIT;
|
||||
Tile tile = null;
|
||||
while (quota > 0) {
|
||||
synchronized (mQueueLock) {
|
||||
tile = mUploadQueue.pop();
|
||||
}
|
||||
if (tile == null) {
|
||||
break;
|
||||
}
|
||||
if (!tile.isContentValid()) {
|
||||
if (tile.mTileState == STATE_DECODED) {
|
||||
tile.updateContent(canvas);
|
||||
--quota;
|
||||
} else {
|
||||
Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tile != null) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the tile to a square at canvas that locates at (x, y) and
|
||||
// has a side length of length.
|
||||
private void drawTile(GLCanvas canvas,
|
||||
int tx, int ty, int level, float x, float y, float length) {
|
||||
RectF source = mSourceRect;
|
||||
RectF target = mTargetRect;
|
||||
target.set(x, y, x + length, y + length);
|
||||
source.set(0, 0, mTileSize, mTileSize);
|
||||
|
||||
Tile tile = getTile(tx, ty, level);
|
||||
if (tile != null) {
|
||||
if (!tile.isContentValid()) {
|
||||
if (tile.mTileState == STATE_DECODED) {
|
||||
if (mUploadQuota > 0) {
|
||||
--mUploadQuota;
|
||||
tile.updateContent(canvas);
|
||||
} else {
|
||||
mRenderComplete = false;
|
||||
}
|
||||
} else if (tile.mTileState != STATE_DECODE_FAIL){
|
||||
mRenderComplete = false;
|
||||
queueForDecode(tile);
|
||||
}
|
||||
}
|
||||
if (drawTile(tile, canvas, source, target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mPreview != null) {
|
||||
int size = mTileSize << level;
|
||||
float scaleX = (float) mPreview.getWidth() / mImageWidth;
|
||||
float scaleY = (float) mPreview.getHeight() / mImageHeight;
|
||||
source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
|
||||
(ty + size) * scaleY);
|
||||
canvas.drawTexture(mPreview, source, target);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean drawTile(
|
||||
Tile tile, GLCanvas canvas, RectF source, RectF target) {
|
||||
while (true) {
|
||||
if (tile.isContentValid()) {
|
||||
canvas.drawTexture(tile, source, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parent can be divided to four quads and tile is one of the four.
|
||||
Tile parent = tile.getParentTile();
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
if (tile.mX == parent.mX) {
|
||||
source.left /= 2f;
|
||||
source.right /= 2f;
|
||||
} else {
|
||||
source.left = (mTileSize + source.left) / 2f;
|
||||
source.right = (mTileSize + source.right) / 2f;
|
||||
}
|
||||
if (tile.mY == parent.mY) {
|
||||
source.top /= 2f;
|
||||
source.bottom /= 2f;
|
||||
} else {
|
||||
source.top = (mTileSize + source.top) / 2f;
|
||||
source.bottom = (mTileSize + source.bottom) / 2f;
|
||||
}
|
||||
tile = parent;
|
||||
}
|
||||
}
|
||||
|
||||
private class Tile extends UploadedTexture {
|
||||
public int mX;
|
||||
public int mY;
|
||||
public int mTileLevel;
|
||||
public Tile mNext;
|
||||
public Bitmap mDecodedTile;
|
||||
public volatile int mTileState = STATE_ACTIVATED;
|
||||
|
||||
public Tile(int x, int y, int level) {
|
||||
mX = x;
|
||||
mY = y;
|
||||
mTileLevel = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFreeBitmap(Bitmap bitmap) {
|
||||
sTilePool.release(bitmap);
|
||||
}
|
||||
|
||||
boolean decode() {
|
||||
// Get a tile from the original image. The tile is down-scaled
|
||||
// by (1 << mTilelevel) from a region in the original image.
|
||||
try {
|
||||
Bitmap reuse = sTilePool.acquire();
|
||||
if (reuse != null && reuse.getWidth() != mTileSize) {
|
||||
reuse = null;
|
||||
}
|
||||
mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "fail to decode tile", t);
|
||||
}
|
||||
return mDecodedTile != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap onGetBitmap() {
|
||||
Utils.assertTrue(mTileState == STATE_DECODED);
|
||||
|
||||
// We need to override the width and height, so that we won't
|
||||
// draw beyond the boundaries.
|
||||
int rightEdge = ((mImageWidth - mX) >> mTileLevel);
|
||||
int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
|
||||
setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge));
|
||||
|
||||
Bitmap bitmap = mDecodedTile;
|
||||
mDecodedTile = null;
|
||||
mTileState = STATE_ACTIVATED;
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// We override getTextureWidth() and getTextureHeight() here, so the
|
||||
// texture can be re-used for different tiles regardless of the actual
|
||||
// size of the tile (which may be small because it is a tile at the
|
||||
// boundary).
|
||||
@Override
|
||||
public int getTextureWidth() {
|
||||
return mTileSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTextureHeight() {
|
||||
return mTileSize;
|
||||
}
|
||||
|
||||
public void update(int x, int y, int level) {
|
||||
mX = x;
|
||||
mY = y;
|
||||
mTileLevel = level;
|
||||
invalidateContent();
|
||||
}
|
||||
|
||||
public Tile getParentTile() {
|
||||
if (mTileLevel + 1 == mLevelCount) {
|
||||
return null;
|
||||
}
|
||||
int size = mTileSize << (mTileLevel + 1);
|
||||
int x = size * (mX / size);
|
||||
int y = size * (mY / size);
|
||||
return getTile(x, y, mTileLevel + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("tile(%s, %s, %s / %s)",
|
||||
mX / mTileSize, mY / mTileSize, mLevel, mLevelCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TileQueue {
|
||||
private Tile mHead;
|
||||
|
||||
public Tile pop() {
|
||||
Tile tile = mHead;
|
||||
if (tile != null) {
|
||||
mHead = tile.mNext;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
public boolean push(Tile tile) {
|
||||
if (contains(tile)) {
|
||||
Log.w(TAG, "Attempting to add a tile already in the queue!");
|
||||
return false;
|
||||
}
|
||||
boolean wasEmpty = mHead == null;
|
||||
tile.mNext = mHead;
|
||||
mHead = tile;
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
private boolean contains(Tile tile) {
|
||||
Tile other = mHead;
|
||||
while (other != null) {
|
||||
if (other == tile) {
|
||||
return true;
|
||||
}
|
||||
other = other.mNext;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clean() {
|
||||
mHead = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class TileDecoder extends Thread {
|
||||
|
||||
public void finishAndWait() {
|
||||
interrupt();
|
||||
try {
|
||||
join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!");
|
||||
}
|
||||
}
|
||||
|
||||
private Tile waitForTile() throws InterruptedException {
|
||||
synchronized (mQueueLock) {
|
||||
while (true) {
|
||||
Tile tile = mDecodeQueue.pop();
|
||||
if (tile != null) {
|
||||
return tile;
|
||||
}
|
||||
mQueueLock.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
Tile tile = waitForTile();
|
||||
decodeTile(tile);
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
// We were finished
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
|
||||
package com.android.photos.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.RectF;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.opengl.GLSurfaceView.Renderer;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Choreographer;
|
||||
import android.view.Choreographer.FrameCallback;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.gallery3d.glrenderer.BasicTexture;
|
||||
import com.android.gallery3d.glrenderer.GLES20Canvas;
|
||||
import com.android.photos.views.TiledImageRenderer.TileSource;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
|
||||
* or {@link BlockingGLTextureView}.
|
||||
*/
|
||||
public class TiledImageView extends FrameLayout {
|
||||
|
||||
private static final boolean USE_TEXTURE_VIEW = false;
|
||||
private static final boolean IS_SUPPORTED =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
|
||||
private static final boolean USE_CHOREOGRAPHER =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
|
||||
|
||||
private BlockingGLTextureView mTextureView;
|
||||
private GLSurfaceView mGLSurfaceView;
|
||||
private boolean mInvalPending = false;
|
||||
private FrameCallback mFrameCallback;
|
||||
|
||||
protected static class ImageRendererWrapper {
|
||||
// Guarded by locks
|
||||
public float scale;
|
||||
public int centerX, centerY;
|
||||
int rotation;
|
||||
public TileSource source;
|
||||
Runnable isReadyCallback;
|
||||
|
||||
// GL thread only
|
||||
TiledImageRenderer image;
|
||||
}
|
||||
|
||||
private float[] mValues = new float[9];
|
||||
|
||||
// -------------------------
|
||||
// Guarded by mLock
|
||||
// -------------------------
|
||||
protected Object mLock = new Object();
|
||||
protected ImageRendererWrapper mRenderer;
|
||||
|
||||
public static boolean isTilingSupported() {
|
||||
return IS_SUPPORTED;
|
||||
}
|
||||
|
||||
public TiledImageView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TiledImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRenderer = new ImageRendererWrapper();
|
||||
mRenderer.image = new TiledImageRenderer(this);
|
||||
View view;
|
||||
if (USE_TEXTURE_VIEW) {
|
||||
mTextureView = new BlockingGLTextureView(context);
|
||||
mTextureView.setRenderer(new TileRenderer());
|
||||
view = mTextureView;
|
||||
} else {
|
||||
mGLSurfaceView = new GLSurfaceView(context);
|
||||
mGLSurfaceView.setEGLContextClientVersion(2);
|
||||
mGLSurfaceView.setRenderer(new TileRenderer());
|
||||
mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||
view = mGLSurfaceView;
|
||||
}
|
||||
addView(view, new LayoutParams(
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
//setTileSource(new ColoredTiles());
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (USE_TEXTURE_VIEW) {
|
||||
mTextureView.destroy();
|
||||
} else {
|
||||
mGLSurfaceView.queueEvent(mFreeTextures);
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable mFreeTextures = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mRenderer.image.freeTextures();
|
||||
}
|
||||
};
|
||||
|
||||
public void onPause() {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (!USE_TEXTURE_VIEW) {
|
||||
mGLSurfaceView.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (!USE_TEXTURE_VIEW) {
|
||||
mGLSurfaceView.onResume();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTileSource(TileSource source, Runnable isReadyCallback) {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mRenderer.source = source;
|
||||
mRenderer.isReadyCallback = isReadyCallback;
|
||||
mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
|
||||
mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
|
||||
mRenderer.rotation = source != null ? source.getRotation() : 0;
|
||||
mRenderer.scale = 0;
|
||||
updateScaleIfNecessaryLocked(mRenderer);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right,
|
||||
int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
updateScaleIfNecessaryLocked(mRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
|
||||
if (renderer == null || renderer.source == null
|
||||
|| renderer.scale > 0 || getWidth() == 0) {
|
||||
return;
|
||||
}
|
||||
renderer.scale = Math.min(
|
||||
(float) getWidth() / (float) renderer.source.getImageWidth(),
|
||||
(float) getHeight() / (float) renderer.source.getImageHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (USE_TEXTURE_VIEW) {
|
||||
mTextureView.render();
|
||||
}
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void setTranslationX(float translationX) {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
super.setTranslationX(translationX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate() {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (USE_TEXTURE_VIEW) {
|
||||
super.invalidate();
|
||||
mTextureView.invalidate();
|
||||
} else {
|
||||
if (USE_CHOREOGRAPHER) {
|
||||
invalOnVsync();
|
||||
} else {
|
||||
mGLSurfaceView.requestRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
private void invalOnVsync() {
|
||||
if (!mInvalPending) {
|
||||
mInvalPending = true;
|
||||
if (mFrameCallback == null) {
|
||||
mFrameCallback = new FrameCallback() {
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
mInvalPending = false;
|
||||
mGLSurfaceView.requestRender();
|
||||
}
|
||||
};
|
||||
}
|
||||
Choreographer.getInstance().postFrameCallback(mFrameCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private RectF mTempRectF = new RectF();
|
||||
public void positionFromMatrix(Matrix matrix) {
|
||||
if (!IS_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
if (mRenderer.source != null) {
|
||||
final int rotation = mRenderer.source.getRotation();
|
||||
final boolean swap = !(rotation % 180 == 0);
|
||||
final int width = swap ? mRenderer.source.getImageHeight()
|
||||
: mRenderer.source.getImageWidth();
|
||||
final int height = swap ? mRenderer.source.getImageWidth()
|
||||
: mRenderer.source.getImageHeight();
|
||||
mTempRectF.set(0, 0, width, height);
|
||||
matrix.mapRect(mTempRectF);
|
||||
matrix.getValues(mValues);
|
||||
int cx = width / 2;
|
||||
int cy = height / 2;
|
||||
float scale = mValues[Matrix.MSCALE_X];
|
||||
int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
|
||||
int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
|
||||
if (rotation == 90 || rotation == 180) {
|
||||
cx += (mTempRectF.left / scale) - xoffset;
|
||||
} else {
|
||||
cx -= (mTempRectF.left / scale) - xoffset;
|
||||
}
|
||||
if (rotation == 180 || rotation == 270) {
|
||||
cy += (mTempRectF.top / scale) - yoffset;
|
||||
} else {
|
||||
cy -= (mTempRectF.top / scale) - yoffset;
|
||||
}
|
||||
mRenderer.scale = scale;
|
||||
mRenderer.centerX = swap ? cy : cx;
|
||||
mRenderer.centerY = swap ? cx : cy;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private class TileRenderer implements Renderer {
|
||||
|
||||
private GLES20Canvas mCanvas;
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
mCanvas = new GLES20Canvas();
|
||||
BasicTexture.invalidateAllTextures();
|
||||
mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
mCanvas.setSize(width, height);
|
||||
mRenderer.image.setViewSize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
mCanvas.clearBuffer();
|
||||
Runnable readyCallback;
|
||||
synchronized (mLock) {
|
||||
readyCallback = mRenderer.isReadyCallback;
|
||||
mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
|
||||
mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
|
||||
mRenderer.scale);
|
||||
}
|
||||
boolean complete = mRenderer.image.draw(mCanvas);
|
||||
if (complete && readyCallback != null) {
|
||||
synchronized (mLock) {
|
||||
// Make sure we don't trample on a newly set callback/source
|
||||
// if it changed while we were rendering
|
||||
if (mRenderer.isReadyCallback == readyCallback) {
|
||||
mRenderer.isReadyCallback = null;
|
||||
}
|
||||
}
|
||||
if (readyCallback != null) {
|
||||
post(readyCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class ColoredTiles implements TileSource {
|
||||
private static final int[] COLORS = new int[] {
|
||||
Color.RED,
|
||||
Color.BLUE,
|
||||
Color.YELLOW,
|
||||
Color.GREEN,
|
||||
Color.CYAN,
|
||||
Color.MAGENTA,
|
||||
Color.WHITE,
|
||||
};
|
||||
|
||||
private Paint mPaint = new Paint();
|
||||
private Canvas mCanvas = new Canvas();
|
||||
|
||||
@Override
|
||||
public int getTileSize() {
|
||||
return 256;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageWidth() {
|
||||
return 16384;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageHeight() {
|
||||
return 8192;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRotation() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
|
||||
int tileSize = getTileSize();
|
||||
if (bitmap == null) {
|
||||
bitmap = Bitmap.createBitmap(tileSize, tileSize,
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
mCanvas.setBitmap(bitmap);
|
||||
mCanvas.drawColor(COLORS[level]);
|
||||
mPaint.setColor(Color.BLACK);
|
||||
mPaint.setTextSize(20);
|
||||
mPaint.setTextAlign(Align.CENTER);
|
||||
mCanvas.drawText(x + "x" + y, 128, 128, mPaint);
|
||||
tileSize <<= level;
|
||||
x /= tileSize;
|
||||
y /= tileSize;
|
||||
mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint);
|
||||
mCanvas.setBitmap(null);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicTexture getPreview() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
/* Copied from Launcher3 */
|
||||
package com.android.wallpapercropper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ScaleGestureDetector.OnScaleGestureListener;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||
|
||||
import com.android.photos.views.TiledImageRenderer.TileSource;
|
||||
import com.android.photos.views.TiledImageView;
|
||||
|
||||
public class CropView extends TiledImageView implements OnScaleGestureListener {
|
||||
|
||||
private ScaleGestureDetector mScaleGestureDetector;
|
||||
private long mTouchDownTime;
|
||||
private float mFirstX, mFirstY;
|
||||
private float mLastX, mLastY;
|
||||
private float mMinScale;
|
||||
private boolean mTouchEnabled = true;
|
||||
private RectF mTempEdges = new RectF();
|
||||
TouchCallback mTouchCallback;
|
||||
|
||||
public interface TouchCallback {
|
||||
void onTouchDown();
|
||||
void onTap();
|
||||
}
|
||||
|
||||
public CropView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CropView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mScaleGestureDetector = new ScaleGestureDetector(context, this);
|
||||
}
|
||||
|
||||
private void getEdgesHelper(RectF edgesOut) {
|
||||
final float width = getWidth();
|
||||
final float height = getHeight();
|
||||
final float imageWidth = mRenderer.source.getImageWidth();
|
||||
final float imageHeight = mRenderer.source.getImageHeight();
|
||||
final float scale = mRenderer.scale;
|
||||
float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f)
|
||||
* scale + width / 2f;
|
||||
float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f)
|
||||
* scale + height / 2f;
|
||||
float leftEdge = centerX - imageWidth / 2f * scale;
|
||||
float rightEdge = centerX + imageWidth / 2f * scale;
|
||||
float topEdge = centerY - imageHeight / 2f * scale;
|
||||
float bottomEdge = centerY + imageHeight / 2f * scale;
|
||||
|
||||
edgesOut.left = leftEdge;
|
||||
edgesOut.right = rightEdge;
|
||||
edgesOut.top = topEdge;
|
||||
edgesOut.bottom = bottomEdge;
|
||||
}
|
||||
|
||||
public RectF getCrop() {
|
||||
final RectF edges = mTempEdges;
|
||||
getEdgesHelper(edges);
|
||||
final float scale = mRenderer.scale;
|
||||
|
||||
float cropLeft = -edges.left / scale;
|
||||
float cropTop = -edges.top / scale;
|
||||
float cropRight = cropLeft + getWidth() / scale;
|
||||
float cropBottom = cropTop + getHeight() / scale;
|
||||
|
||||
return new RectF(cropLeft, cropTop, cropRight, cropBottom);
|
||||
}
|
||||
|
||||
public Point getSourceDimensions() {
|
||||
return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
|
||||
}
|
||||
|
||||
public void setTileSource(TileSource source, Runnable isReadyCallback) {
|
||||
super.setTileSource(source, isReadyCallback);
|
||||
updateMinScale(getWidth(), getHeight(), source, true);
|
||||
}
|
||||
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
updateMinScale(w, h, mRenderer.source, false);
|
||||
}
|
||||
|
||||
public void setScale(float scale) {
|
||||
synchronized (mLock) {
|
||||
mRenderer.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
|
||||
synchronized (mLock) {
|
||||
if (resetScale) {
|
||||
mRenderer.scale = 1;
|
||||
}
|
||||
if (source != null) {
|
||||
mMinScale = Math.max(w / (float) source.getImageWidth(),
|
||||
h / (float) source.getImageHeight());
|
||||
mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
// Don't need the lock because this will only fire inside of
|
||||
// onTouchEvent
|
||||
mRenderer.scale *= detector.getScaleFactor();
|
||||
mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
}
|
||||
|
||||
public void moveToUpperLeft() {
|
||||
if (getWidth() == 0 || getHeight() == 0) {
|
||||
final ViewTreeObserver observer = getViewTreeObserver();
|
||||
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
|
||||
public void onGlobalLayout() {
|
||||
moveToUpperLeft();
|
||||
getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
final RectF edges = mTempEdges;
|
||||
getEdgesHelper(edges);
|
||||
final float scale = mRenderer.scale;
|
||||
mRenderer.centerX += Math.ceil(edges.left / scale);
|
||||
mRenderer.centerY += Math.ceil(edges.top / scale);
|
||||
}
|
||||
|
||||
public void setTouchEnabled(boolean enabled) {
|
||||
mTouchEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setTouchCallback(TouchCallback cb) {
|
||||
mTouchCallback = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
int action = event.getActionMasked();
|
||||
final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
|
||||
final int skipIndex = pointerUp ? event.getActionIndex() : -1;
|
||||
|
||||
// Determine focal point
|
||||
float sumX = 0, sumY = 0;
|
||||
final int count = event.getPointerCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (skipIndex == i)
|
||||
continue;
|
||||
sumX += event.getX(i);
|
||||
sumY += event.getY(i);
|
||||
}
|
||||
final int div = pointerUp ? count - 1 : count;
|
||||
float x = sumX / div;
|
||||
float y = sumY / div;
|
||||
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mFirstX = x;
|
||||
mFirstY = y;
|
||||
mTouchDownTime = System.currentTimeMillis();
|
||||
if (mTouchCallback != null) {
|
||||
mTouchCallback.onTouchDown();
|
||||
}
|
||||
} else if (action == MotionEvent.ACTION_UP) {
|
||||
ViewConfiguration config = ViewConfiguration.get(getContext());
|
||||
|
||||
float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
|
||||
float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
|
||||
long now = System.currentTimeMillis();
|
||||
// only do this if it's a small movement
|
||||
if (mTouchCallback != null &&
|
||||
squaredDist < slop &&
|
||||
now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
|
||||
mTouchCallback.onTap();
|
||||
}
|
||||
}
|
||||
|
||||
if (!mTouchEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
mScaleGestureDetector.onTouchEvent(event);
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
mRenderer.centerX += (mLastX - x) / mRenderer.scale;
|
||||
mRenderer.centerY += (mLastY - y) / mRenderer.scale;
|
||||
invalidate();
|
||||
break;
|
||||
}
|
||||
if (mRenderer.source != null) {
|
||||
// Adjust position so that the wallpaper covers the entire area
|
||||
// of the screen
|
||||
final RectF edges = mTempEdges;
|
||||
getEdgesHelper(edges);
|
||||
final float scale = mRenderer.scale;
|
||||
if (edges.left > 0) {
|
||||
mRenderer.centerX += Math.ceil(edges.left / scale);
|
||||
}
|
||||
if (edges.right < getWidth()) {
|
||||
mRenderer.centerX += (edges.right - getWidth()) / scale;
|
||||
}
|
||||
if (edges.top > 0) {
|
||||
mRenderer.centerY += Math.ceil(edges.top / scale);
|
||||
}
|
||||
if (edges.bottom < getHeight()) {
|
||||
mRenderer.centerY += (edges.bottom - getHeight()) / scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLastX = x;
|
||||
mLastY = y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.
|
||||
*/
|
||||
/* Copied from Launcher3 */
|
||||
package com.android.wallpapercropper;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.app.WallpaperManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.gallery3d.common.Utils;
|
||||
import com.android.photos.BitmapRegionTileSource;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WallpaperCropActivity extends Activity {
|
||||
private static final String LOGTAG = "Launcher3.CropActivity";
|
||||
|
||||
protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
|
||||
protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
|
||||
private static final int DEFAULT_COMPRESS_QUALITY = 90;
|
||||
/**
|
||||
* The maximum bitmap size we allow to be returned through the intent.
|
||||
* Intents have a maximum of 1MB in total size. However, the Bitmap seems to
|
||||
* have some overhead to hit so that we go way below the limit here to make
|
||||
* sure the intent stays below 1MB.We should consider just returning a byte
|
||||
* array instead of a Bitmap instance to avoid overhead.
|
||||
*/
|
||||
public static final int MAX_BMAP_IN_INTENT = 750000;
|
||||
private static final float WALLPAPER_SCREENS_SPAN = 2f;
|
||||
|
||||
protected CropView mCropView;
|
||||
protected Uri mUri;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
init();
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
setContentView(R.layout.wallpaper_cropper);
|
||||
|
||||
mCropView = (CropView) findViewById(R.id.cropView);
|
||||
|
||||
Intent cropIntent = this.getIntent();
|
||||
final Uri imageUri = cropIntent.getData();
|
||||
|
||||
mCropView.setTileSource(new BitmapRegionTileSource(this, imageUri, 1024, 0), null);
|
||||
mCropView.setTouchEnabled(true);
|
||||
// Action bar
|
||||
// Show the custom action bar view
|
||||
final ActionBar actionBar = getActionBar();
|
||||
actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
|
||||
actionBar.getCustomView().setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean finishActivityWhenDone = true;
|
||||
cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String getSharedPreferencesKey() {
|
||||
return WallpaperCropActivity.class.getName();
|
||||
}
|
||||
|
||||
// As a ratio of screen height, the total distance we want the parallax effect to span
|
||||
// horizontally
|
||||
private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
|
||||
float aspectRatio = width / (float) height;
|
||||
|
||||
// At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
|
||||
// At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
|
||||
// We will use these two data points to extrapolate how much the wallpaper parallax effect
|
||||
// to span (ie travel) at any aspect ratio:
|
||||
|
||||
final float ASPECT_RATIO_LANDSCAPE = 16/10f;
|
||||
final float ASPECT_RATIO_PORTRAIT = 10/16f;
|
||||
final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
|
||||
final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
|
||||
|
||||
// To find out the desired width at different aspect ratios, we use the following two
|
||||
// formulas, where the coefficient on x is the aspect ratio (width/height):
|
||||
// (16/10)x + y = 1.5
|
||||
// (10/16)x + y = 1.2
|
||||
// We solve for x and y and end up with a final formula:
|
||||
final float x =
|
||||
(WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
|
||||
(ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
|
||||
final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
|
||||
return x * aspectRatio + y;
|
||||
}
|
||||
|
||||
static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
|
||||
Point minDims = new Point();
|
||||
Point maxDims = new Point();
|
||||
windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
|
||||
|
||||
int maxDim = Math.max(maxDims.x, maxDims.y);
|
||||
final int minDim = Math.min(minDims.x, minDims.y);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
Point realSize = new Point();
|
||||
windowManager.getDefaultDisplay().getRealSize(realSize);
|
||||
maxDim = Math.max(realSize.x, realSize.y);
|
||||
}
|
||||
|
||||
// We need to ensure that there is enough extra space in the wallpaper
|
||||
// for the intended
|
||||
// parallax effects
|
||||
final int defaultWidth, defaultHeight;
|
||||
if (isScreenLarge(res)) {
|
||||
defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
|
||||
defaultHeight = maxDim;
|
||||
} else {
|
||||
defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
|
||||
defaultHeight = maxDim;
|
||||
}
|
||||
return new Point(defaultWidth, defaultHeight);
|
||||
}
|
||||
|
||||
protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
|
||||
|
||||
BitmapCropTask cropTask = new BitmapCropTask(this,
|
||||
filePath, null, 0, 0, true, false, null);
|
||||
final Point bounds = cropTask.getImageBounds();
|
||||
Runnable onEndCrop = new Runnable() {
|
||||
public void run() {
|
||||
updateWallpaperDimensions(bounds.x, bounds.y);
|
||||
if (finishActivityWhenDone) {
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
cropTask.setOnEndRunnable(onEndCrop);
|
||||
cropTask.setNoCrop(true);
|
||||
cropTask.execute();
|
||||
}
|
||||
|
||||
protected void cropImageAndSetWallpaper(
|
||||
Resources res, int resId, final boolean finishActivityWhenDone) {
|
||||
// crop this image and scale it down to the default wallpaper size for
|
||||
// this device
|
||||
Point inSize = mCropView.getSourceDimensions();
|
||||
Point outSize = getDefaultWallpaperSize(getResources(),
|
||||
getWindowManager());
|
||||
RectF crop = getMaxCropRect(
|
||||
inSize.x, inSize.y, outSize.x, outSize.y, false);
|
||||
Runnable onEndCrop = new Runnable() {
|
||||
public void run() {
|
||||
// Passing 0, 0 will cause launcher to revert to using the
|
||||
// default wallpaper size
|
||||
updateWallpaperDimensions(0, 0);
|
||||
if (finishActivityWhenDone) {
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
BitmapCropTask cropTask = new BitmapCropTask(res, resId,
|
||||
crop, outSize.x, outSize.y,
|
||||
true, false, onEndCrop);
|
||||
cropTask.execute();
|
||||
}
|
||||
|
||||
private static boolean isScreenLarge(Resources res) {
|
||||
Configuration config = res.getConfiguration();
|
||||
return config.smallestScreenWidthDp >= 720;
|
||||
}
|
||||
|
||||
protected void cropImageAndSetWallpaper(Uri uri,
|
||||
OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
|
||||
// Get the crop
|
||||
Point inSize = mCropView.getSourceDimensions();
|
||||
|
||||
Point minDims = new Point();
|
||||
Point maxDims = new Point();
|
||||
Display d = getWindowManager().getDefaultDisplay();
|
||||
d.getCurrentSizeRange(minDims, maxDims);
|
||||
|
||||
Point displaySize = new Point();
|
||||
d.getSize(displaySize);
|
||||
|
||||
int maxDim = Math.max(maxDims.x, maxDims.y);
|
||||
final int minDim = Math.min(minDims.x, minDims.y);
|
||||
int defaultWidth;
|
||||
if (isScreenLarge(getResources())) {
|
||||
defaultWidth = (int) (maxDim *
|
||||
wallpaperTravelToScreenWidthRatio(maxDim, minDim));
|
||||
} else {
|
||||
defaultWidth = Math.max((int)
|
||||
(minDim * WALLPAPER_SCREENS_SPAN), maxDim);
|
||||
}
|
||||
|
||||
boolean isPortrait = displaySize.x < displaySize.y;
|
||||
int portraitHeight;
|
||||
if (isPortrait) {
|
||||
portraitHeight = mCropView.getHeight();
|
||||
} else {
|
||||
// TODO: how to actually get the proper portrait height?
|
||||
// This is not quite right:
|
||||
portraitHeight = Math.max(maxDims.x, maxDims.y);
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT >=
|
||||
android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
Point realSize = new Point();
|
||||
d.getRealSize(realSize);
|
||||
portraitHeight = Math.max(realSize.x, realSize.y);
|
||||
}
|
||||
// Get the crop
|
||||
RectF cropRect = mCropView.getCrop();
|
||||
float cropScale = mCropView.getWidth() / (float) cropRect.width();
|
||||
|
||||
// ADJUST CROP WIDTH
|
||||
// Extend the crop all the way to the right, for parallax
|
||||
float extraSpaceToRight = inSize.x - cropRect.right;
|
||||
// Cap the amount of extra width
|
||||
float maxExtraSpace = defaultWidth / cropScale - cropRect.width();
|
||||
extraSpaceToRight = Math.min(extraSpaceToRight, maxExtraSpace);
|
||||
|
||||
cropRect.right += extraSpaceToRight;
|
||||
|
||||
// ADJUST CROP HEIGHT
|
||||
if (isPortrait) {
|
||||
cropRect.bottom = cropRect.top + portraitHeight / cropScale;
|
||||
} else { // LANDSCAPE
|
||||
float extraPortraitHeight =
|
||||
portraitHeight / cropScale - cropRect.height();
|
||||
float expandHeight =
|
||||
Math.min(Math.min(inSize.y - cropRect.bottom, cropRect.top),
|
||||
extraPortraitHeight / 2);
|
||||
cropRect.top -= expandHeight;
|
||||
cropRect.bottom += expandHeight;
|
||||
}
|
||||
final int outWidth = (int) Math.round(cropRect.width() * cropScale);
|
||||
final int outHeight = (int) Math.round(cropRect.height() * cropScale);
|
||||
|
||||
Runnable onEndCrop = new Runnable() {
|
||||
public void run() {
|
||||
updateWallpaperDimensions(outWidth, outHeight);
|
||||
if (finishActivityWhenDone) {
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
BitmapCropTask cropTask = new BitmapCropTask(uri,
|
||||
cropRect, outWidth, outHeight, true, false, onEndCrop);
|
||||
if (onBitmapCroppedHandler != null) {
|
||||
cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
|
||||
}
|
||||
cropTask.execute();
|
||||
}
|
||||
|
||||
public interface OnBitmapCroppedHandler {
|
||||
public void onBitmapCropped(byte[] imageBytes);
|
||||
}
|
||||
|
||||
protected class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
|
||||
Uri mInUri = null;
|
||||
Context mContext;
|
||||
String mInFilePath;
|
||||
byte[] mInImageBytes;
|
||||
int mInResId = 0;
|
||||
InputStream mInStream;
|
||||
RectF mCropBounds = null;
|
||||
int mOutWidth, mOutHeight;
|
||||
int mRotation = 0; // for now
|
||||
protected final WallpaperManager mWPManager;
|
||||
String mOutputFormat = "jpg"; // for now
|
||||
boolean mSetWallpaper;
|
||||
boolean mSaveCroppedBitmap;
|
||||
Bitmap mCroppedBitmap;
|
||||
Runnable mOnEndRunnable;
|
||||
Resources mResources;
|
||||
OnBitmapCroppedHandler mOnBitmapCroppedHandler;
|
||||
boolean mNoCrop;
|
||||
|
||||
public BitmapCropTask(Context c, String filePath,
|
||||
RectF cropBounds, int outWidth, int outHeight,
|
||||
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
|
||||
mContext = c;
|
||||
mInFilePath = filePath;
|
||||
mWPManager = WallpaperManager.getInstance(getApplicationContext());
|
||||
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
|
||||
}
|
||||
|
||||
public BitmapCropTask(byte[] imageBytes,
|
||||
RectF cropBounds, int outWidth, int outHeight,
|
||||
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
|
||||
mInImageBytes = imageBytes;
|
||||
mWPManager = WallpaperManager.getInstance(getApplicationContext());
|
||||
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
|
||||
}
|
||||
|
||||
public BitmapCropTask(Uri inUri,
|
||||
RectF cropBounds, int outWidth, int outHeight,
|
||||
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
|
||||
mInUri = inUri;
|
||||
mWPManager = WallpaperManager.getInstance(getApplicationContext());
|
||||
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
|
||||
}
|
||||
|
||||
public BitmapCropTask(Resources res, int inResId,
|
||||
RectF cropBounds, int outWidth, int outHeight,
|
||||
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
|
||||
mInResId = inResId;
|
||||
mResources = res;
|
||||
mWPManager = WallpaperManager.getInstance(getApplicationContext());
|
||||
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
|
||||
}
|
||||
|
||||
private void init(RectF cropBounds, int outWidth, int outHeight,
|
||||
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
|
||||
mCropBounds = cropBounds;
|
||||
mOutWidth = outWidth;
|
||||
mOutHeight = outHeight;
|
||||
mSetWallpaper = setWallpaper;
|
||||
mSaveCroppedBitmap = saveCroppedBitmap;
|
||||
mOnEndRunnable = onEndRunnable;
|
||||
}
|
||||
|
||||
public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
|
||||
mOnBitmapCroppedHandler = handler;
|
||||
}
|
||||
|
||||
public void setNoCrop(boolean value) {
|
||||
mNoCrop = value;
|
||||
}
|
||||
|
||||
public void setOnEndRunnable(Runnable onEndRunnable) {
|
||||
mOnEndRunnable = onEndRunnable;
|
||||
}
|
||||
|
||||
// Helper to setup input stream
|
||||
private void regenerateInputStream() {
|
||||
if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
|
||||
Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
|
||||
"image byte array given");
|
||||
} else {
|
||||
Utils.closeSilently(mInStream);
|
||||
try {
|
||||
if (mInUri != null) {
|
||||
mInStream = new BufferedInputStream(
|
||||
getContentResolver().openInputStream(mInUri));
|
||||
} else if (mInFilePath != null) {
|
||||
mInStream = mContext.openFileInput(mInFilePath);
|
||||
} else if (mInImageBytes != null) {
|
||||
mInStream = new BufferedInputStream(
|
||||
new ByteArrayInputStream(mInImageBytes));
|
||||
} else {
|
||||
mInStream = new BufferedInputStream(
|
||||
mResources.openRawResource(mInResId));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Point getImageBounds() {
|
||||
regenerateInputStream();
|
||||
if (mInStream != null) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(mInStream, null, options);
|
||||
if (options.outWidth != 0 && options.outHeight != 0) {
|
||||
return new Point(options.outWidth, options.outHeight);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setCropBounds(RectF cropBounds) {
|
||||
mCropBounds = cropBounds;
|
||||
}
|
||||
|
||||
public Bitmap getCroppedBitmap() {
|
||||
return mCroppedBitmap;
|
||||
}
|
||||
public boolean cropBitmap() {
|
||||
boolean failure = false;
|
||||
|
||||
regenerateInputStream();
|
||||
|
||||
if (mNoCrop && mInStream != null) {
|
||||
try {
|
||||
mWPManager.setStream(mInStream);
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
|
||||
failure = true;
|
||||
}
|
||||
if (mOnEndRunnable != null) {
|
||||
mOnEndRunnable.run();
|
||||
}
|
||||
return !failure;
|
||||
}
|
||||
if (mInStream != null) {
|
||||
// Find crop bounds (scaled to original image size)
|
||||
Rect roundedTrueCrop = new Rect();
|
||||
mCropBounds.roundOut(roundedTrueCrop);
|
||||
|
||||
if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
|
||||
Log.w(LOGTAG, "crop has bad values for full size image");
|
||||
failure = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// See how much we're reducing the size of the image
|
||||
int scaleDownSampleSize = Math.min(roundedTrueCrop.width() / mOutWidth,
|
||||
roundedTrueCrop.height() / mOutHeight);
|
||||
|
||||
// Attempt to open a region decoder
|
||||
BitmapRegionDecoder decoder = null;
|
||||
try {
|
||||
decoder = BitmapRegionDecoder.newInstance(mInStream, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
|
||||
}
|
||||
|
||||
Bitmap crop = null;
|
||||
if (decoder != null) {
|
||||
// Do region decoding to get crop bitmap
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
if (scaleDownSampleSize > 1) {
|
||||
options.inSampleSize = scaleDownSampleSize;
|
||||
}
|
||||
crop = decoder.decodeRegion(roundedTrueCrop, options);
|
||||
decoder.recycle();
|
||||
}
|
||||
|
||||
if (crop == null) {
|
||||
// BitmapRegionDecoder has failed, try to crop in-memory
|
||||
regenerateInputStream();
|
||||
Bitmap fullSize = null;
|
||||
if (mInStream != null) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
if (scaleDownSampleSize > 1) {
|
||||
options.inSampleSize = scaleDownSampleSize;
|
||||
}
|
||||
fullSize = BitmapFactory.decodeStream(mInStream, null, options);
|
||||
}
|
||||
if (fullSize != null) {
|
||||
crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
|
||||
roundedTrueCrop.top, roundedTrueCrop.width(),
|
||||
roundedTrueCrop.height());
|
||||
}
|
||||
}
|
||||
|
||||
if (crop == null) {
|
||||
Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
|
||||
failure = true;
|
||||
return false;
|
||||
}
|
||||
if (mOutWidth > 0 && mOutHeight > 0) {
|
||||
Matrix m = new Matrix();
|
||||
RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
|
||||
if (mRotation > 0) {
|
||||
m.setRotate(mRotation);
|
||||
m.mapRect(cropRect);
|
||||
}
|
||||
RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
|
||||
m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
|
||||
m.preRotate(mRotation);
|
||||
Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
|
||||
(int) returnRect.height(), Bitmap.Config.ARGB_8888);
|
||||
if (tmp != null) {
|
||||
Canvas c = new Canvas(tmp);
|
||||
c.drawBitmap(crop, m, new Paint());
|
||||
crop = tmp;
|
||||
}
|
||||
} else if (mRotation > 0) {
|
||||
Matrix m = new Matrix();
|
||||
m.setRotate(mRotation);
|
||||
Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
|
||||
crop.getHeight(), m, true);
|
||||
if (tmp != null) {
|
||||
crop = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
if (mSaveCroppedBitmap) {
|
||||
mCroppedBitmap = crop;
|
||||
}
|
||||
|
||||
// Get output compression format
|
||||
CompressFormat cf =
|
||||
convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
|
||||
|
||||
// Compress to byte array
|
||||
ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
|
||||
if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
|
||||
// If we need to set to the wallpaper, set it
|
||||
if (mSetWallpaper && mWPManager != null) {
|
||||
if (mWPManager == null) {
|
||||
Log.w(LOGTAG, "no wallpaper manager");
|
||||
failure = true;
|
||||
} else {
|
||||
try {
|
||||
byte[] outByteArray = tmpOut.toByteArray();
|
||||
mWPManager.setStream(new ByteArrayInputStream(outByteArray));
|
||||
if (mOnBitmapCroppedHandler != null) {
|
||||
mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
|
||||
failure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mOnEndRunnable != null) {
|
||||
mOnEndRunnable.run();
|
||||
}
|
||||
} else {
|
||||
Log.w(LOGTAG, "cannot compress bitmap");
|
||||
failure = true;
|
||||
}
|
||||
}
|
||||
return !failure; // True if any of the operations failed
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
return cropBitmap();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateWallpaperDimensions(int width, int height) {
|
||||
String spKey = getSharedPreferencesKey();
|
||||
SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sp.edit();
|
||||
if (width != 0 && height != 0) {
|
||||
editor.putInt(WALLPAPER_WIDTH_KEY, width);
|
||||
editor.putInt(WALLPAPER_HEIGHT_KEY, height);
|
||||
} else {
|
||||
editor.remove(WALLPAPER_WIDTH_KEY);
|
||||
editor.remove(WALLPAPER_HEIGHT_KEY);
|
||||
}
|
||||
editor.commit();
|
||||
|
||||
suggestWallpaperDimension(getResources(),
|
||||
sp, getWindowManager(), WallpaperManager.getInstance(this));
|
||||
}
|
||||
|
||||
static public void suggestWallpaperDimension(Resources res,
|
||||
final SharedPreferences sharedPrefs,
|
||||
WindowManager windowManager,
|
||||
final WallpaperManager wallpaperManager) {
|
||||
final Point defaultWallpaperSize =
|
||||
WallpaperCropActivity.getDefaultWallpaperSize(res, windowManager);
|
||||
|
||||
new Thread("suggestWallpaperDimension") {
|
||||
public void run() {
|
||||
// If we have saved a wallpaper width/height, use that instead
|
||||
int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWallpaperSize.x);
|
||||
int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultWallpaperSize.y);
|
||||
wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
|
||||
protected static RectF getMaxCropRect(
|
||||
int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
|
||||
RectF cropRect = new RectF();
|
||||
// Get a crop rect that will fit this
|
||||
if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
|
||||
cropRect.top = 0;
|
||||
cropRect.bottom = inHeight;
|
||||
cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
|
||||
cropRect.right = inWidth - cropRect.left;
|
||||
if (leftAligned) {
|
||||
cropRect.right -= cropRect.left;
|
||||
cropRect.left = 0;
|
||||
}
|
||||
} else {
|
||||
cropRect.left = 0;
|
||||
cropRect.right = inWidth;
|
||||
cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
|
||||
cropRect.bottom = inHeight - cropRect.top;
|
||||
}
|
||||
return cropRect;
|
||||
}
|
||||
|
||||
protected static CompressFormat convertExtensionToCompressFormat(String extension) {
|
||||
return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
|
||||
}
|
||||
|
||||
protected static String getFileExtension(String requestFormat) {
|
||||
String outputFormat = (requestFormat == null)
|
||||
? "jpg"
|
||||
: requestFormat;
|
||||
outputFormat = outputFormat.toLowerCase();
|
||||
return (outputFormat.equals("png") || outputFormat.equals("gif"))
|
||||
? "png" // We don't support gif compression.
|
||||
: "jpg";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user