am d2f15728: am 772bbcc7: Merge "Revert "Revert "Move wallpaper picker code to a separate directory""" into jb-ub-now-kermit

* commit 'd2f157283c599e49083a89b5c78c64a9258fbe1a':
  Revert "Revert "Move wallpaper picker code to a separate directory""
This commit is contained in:
Michael Jurka
2014-02-14 20:54:33 +00:00
committed by Android Git Automerger
91 changed files with 194 additions and 65 deletions
@@ -1,260 +0,0 @@
/*
* 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");
}
}
-340
View File
@@ -1,340 +0,0 @@
/*
* 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("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '\"': sb.append("&quot;"); break;
case '\'': sb.append("&#039;"); break;
case '&': sb.append("&amp;"); 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));
}
}
@@ -1,48 +0,0 @@
/*
* 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;
}
}
@@ -1,136 +0,0 @@
/*
* 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);
}
}
@@ -1,348 +0,0 @@
/*
* 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
@@ -1,23 +0,0 @@
/*
* 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);
}
}
@@ -1,196 +0,0 @@
/*
* 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);
}
}
@@ -1,518 +0,0 @@
/*
* 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;
}
}
}
@@ -1,916 +0,0 @@
/*
* 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();
}
}
@@ -1,92 +0,0 @@
/*
* 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
-152
View File
@@ -1,152 +0,0 @@
/*
* 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;
}
}
-31
View File
@@ -1,31 +0,0 @@
/*
* 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;
}
@@ -1,39 +0,0 @@
/*
* 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;
}
}
@@ -1,56 +0,0 @@
/*
* 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;
}
}
@@ -1,88 +0,0 @@
/*
* 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;
}
}
@@ -1,212 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,54 +0,0 @@
/*
* 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;
}
}
@@ -1,217 +0,0 @@
/*
* 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
@@ -1,42 +0,0 @@
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();
}
}
@@ -1,33 +0,0 @@
/*
* 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);
}
@@ -1,41 +0,0 @@
/*
* 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;
}
}
@@ -1,73 +0,0 @@
/*
* 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;
}
}
@@ -1,44 +0,0 @@
/*
* 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();
}
@@ -1,298 +0,0 @@
/*
* 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();
}
}
@@ -1,60 +0,0 @@
/*
* 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];
}
}
-322
View File
@@ -1,322 +0,0 @@
/*
* 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.launcher3;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.ViewConfiguration;
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 mCenterX, mCenterY;
private float mMinScale;
private boolean mTouchEnabled = true;
private RectF mTempEdges = new RectF();
private float[] mTempPoint = new float[] { 0, 0 };
private float[] mTempCoef = new float[] { 0, 0 };
private float[] mTempAdjustment = new float[] { 0, 0 };
private float[] mTempImageDims = new float[] { 0, 0 };
private float[] mTempRendererCenter = new float[] { 0, 0 };
TouchCallback mTouchCallback;
Matrix mRotateMatrix;
Matrix mInverseRotateMatrix;
public interface TouchCallback {
void onTouchDown();
void onTap();
void onTouchUp();
}
public CropView(Context context) {
this(context, null);
}
public CropView(Context context, AttributeSet attrs) {
super(context, attrs);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mRotateMatrix = new Matrix();
mInverseRotateMatrix = new Matrix();
}
private float[] getImageDims() {
final float imageWidth = mRenderer.source.getImageWidth();
final float imageHeight = mRenderer.source.getImageHeight();
float[] imageDims = mTempImageDims;
imageDims[0] = imageWidth;
imageDims[1] = imageHeight;
mRotateMatrix.mapPoints(imageDims);
imageDims[0] = Math.abs(imageDims[0]);
imageDims[1] = Math.abs(imageDims[1]);
return imageDims;
}
private void getEdgesHelper(RectF edgesOut) {
final float width = getWidth();
final float height = getHeight();
final float[] imageDims = getImageDims();
final float imageWidth = imageDims[0];
final float imageHeight = imageDims[1];
float initialCenterX = mRenderer.source.getImageWidth() / 2f;
float initialCenterY = mRenderer.source.getImageHeight() / 2f;
float[] rendererCenter = mTempRendererCenter;
rendererCenter[0] = mCenterX - initialCenterX;
rendererCenter[1] = mCenterY - initialCenterY;
mRotateMatrix.mapPoints(rendererCenter);
rendererCenter[0] += imageWidth / 2;
rendererCenter[1] += imageHeight / 2;
final float scale = mRenderer.scale;
float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
* scale + width / 2f;
float centerY = (height / 2f - rendererCenter[1] + (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 int getImageRotation() {
return mRenderer.rotation;
}
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);
mCenterX = mRenderer.centerX;
mCenterY = mRenderer.centerY;
mRotateMatrix.reset();
mRotateMatrix.setRotate(mRenderer.rotation);
mInverseRotateMatrix.reset();
mInverseRotateMatrix.setRotate(-mRenderer.rotation);
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) {
final float[] imageDims = getImageDims();
final float imageWidth = imageDims[0];
final float imageHeight = imageDims[1];
mMinScale = Math.max(w / imageWidth, h / imageHeight);
mRenderer.scale =
Math.max(mMinScale, resetScale ? Float.MIN_VALUE : 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 moveToLeft() {
if (getWidth() == 0 || getHeight() == 0) {
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
moveToLeft();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
mCenterX += Math.ceil(edges.left / scale);
updateCenter();
}
private void updateCenter() {
mRenderer.centerX = Math.round(mCenterX);
mRenderer.centerY = Math.round(mCenterY);
}
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();
if (mTouchCallback != null) {
// only do this if it's a small movement
if (squaredDist < slop &&
now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
mTouchCallback.onTap();
}
mTouchCallback.onTouchUp();
}
}
if (!mTouchEnabled) {
return true;
}
synchronized (mLock) {
mScaleGestureDetector.onTouchEvent(event);
switch (action) {
case MotionEvent.ACTION_MOVE:
float[] point = mTempPoint;
point[0] = (mLastX - x) / mRenderer.scale;
point[1] = (mLastY - y) / mRenderer.scale;
mInverseRotateMatrix.mapPoints(point);
mCenterX += point[0];
mCenterY += point[1];
updateCenter();
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;
float[] coef = mTempCoef;
coef[0] = 1;
coef[1] = 1;
mRotateMatrix.mapPoints(coef);
float[] adjustment = mTempAdjustment;
mTempAdjustment[0] = 0;
mTempAdjustment[1] = 0;
if (edges.left > 0) {
adjustment[0] = edges.left / scale;
} else if (edges.right < getWidth()) {
adjustment[0] = (edges.right - getWidth()) / scale;
}
if (edges.top > 0) {
adjustment[1] = FloatMath.ceil(edges.top / scale);
} else if (edges.bottom < getHeight()) {
adjustment[1] = (edges.bottom - getHeight()) / scale;
}
for (int dim = 0; dim <= 1; dim++) {
if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]);
}
mInverseRotateMatrix.mapPoints(adjustment);
mCenterX += adjustment[0];
mCenterY += adjustment[1];
updateCenter();
}
}
mLastX = x;
mLastY = y;
return true;
}
}
@@ -1,836 +0,0 @@
/*
* 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.launcher3;
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 android.widget.Toast;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.exif.ExifInterface;
import com.android.photos.BitmapRegionTileSource;
import com.android.photos.BitmapRegionTileSource.BitmapSource;
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 static Point sDefaultWallpaperSize;
protected CropView mCropView;
protected Uri mUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
init();
if (!enableRotation()) {
setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
}
}
protected void init() {
setContentView(R.layout.wallpaper_cropper);
mCropView = (CropView) findViewById(R.id.cropView);
Intent cropIntent = getIntent();
final Uri imageUri = cropIntent.getData();
if (imageUri == null) {
Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
finish();
return;
}
// 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);
}
});
// Load image in background
final BitmapRegionTileSource.UriBitmapSource bitmapSource =
new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
Runnable onLoad = new Runnable() {
public void run() {
if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
Toast.makeText(WallpaperCropActivity.this,
getString(R.string.wallpaper_load_fail),
Toast.LENGTH_LONG).show();
finish();
}
}
};
setCropViewTileSource(bitmapSource, true, false, onLoad);
}
public void setCropViewTileSource(
final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
final boolean moveToLeft, final Runnable postExecute) {
final Context context = WallpaperCropActivity.this;
final View progressView = findViewById(R.id.loading);
final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void...args) {
if (!isCancelled()) {
bitmapSource.loadInBackground();
}
return null;
}
protected void onPostExecute(Void arg) {
if (!isCancelled()) {
progressView.setVisibility(View.INVISIBLE);
if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
mCropView.setTileSource(
new BitmapRegionTileSource(context, bitmapSource), null);
mCropView.setTouchEnabled(touchEnabled);
if (moveToLeft) {
mCropView.moveToLeft();
}
}
}
if (postExecute != null) {
postExecute.run();
}
}
};
// We don't want to show the spinner every time we load an image, because that would be
// annoying; instead, only start showing the spinner if loading the image has taken
// longer than 1 sec (ie 1000 ms)
progressView.postDelayed(new Runnable() {
public void run() {
if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
progressView.setVisibility(View.VISIBLE);
}
}
}, 1000);
loadBitmapTask.execute();
}
public boolean enableRotation() {
return getResources().getBoolean(R.bool.allow_rotation);
}
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) {
if (sDefaultWallpaperSize == null) {
Point minDims = new Point();
Point maxDims = new Point();
windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
int maxDim = Math.max(maxDims.x, maxDims.y);
int minDim = Math.max(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);
minDim = Math.min(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;
}
sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
}
return sDefaultWallpaperSize;
}
public static int getRotationFromExif(String path) {
return getRotationFromExifHelper(path, null, 0, null, null);
}
public static int getRotationFromExif(Context context, Uri uri) {
return getRotationFromExifHelper(null, null, 0, context, uri);
}
public static int getRotationFromExif(Resources res, int resId) {
return getRotationFromExifHelper(null, res, resId, null, null);
}
private static int getRotationFromExifHelper(
String path, Resources res, int resId, Context context, Uri uri) {
ExifInterface ei = new ExifInterface();
InputStream is = null;
BufferedInputStream bis = null;
try {
if (path != null) {
ei.readExif(path);
} else if (uri != null) {
is = context.getContentResolver().openInputStream(uri);
bis = new BufferedInputStream(is);
ei.readExif(bis);
} else {
is = res.openRawResource(resId);
bis = new BufferedInputStream(is);
ei.readExif(bis);
}
Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
if (ori != null) {
return ExifInterface.getRotationForOrientationValue(ori.shortValue());
}
} catch (IOException e) {
Log.w(LOGTAG, "Getting exif data failed", e);
} finally {
Utils.closeSilently(bis);
Utils.closeSilently(is);
}
return 0;
}
protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
int rotation = getRotationFromExif(filePath);
BitmapCropTask cropTask = new BitmapCropTask(
this, filePath, null, rotation, 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
int rotation = getRotationFromExif(res, resId);
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(this, res, resId,
crop, rotation, 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
boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
Display d = getWindowManager().getDefaultDisplay();
Point displaySize = new Point();
d.getSize(displaySize);
boolean isPortrait = displaySize.x < displaySize.y;
Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
getWindowManager());
// Get the crop
RectF cropRect = mCropView.getCrop();
int cropRotation = mCropView.getImageRotation();
float cropScale = mCropView.getWidth() / (float) cropRect.width();
Point inSize = mCropView.getSourceDimensions();
Matrix rotateMatrix = new Matrix();
rotateMatrix.setRotate(cropRotation);
float[] rotatedInSize = new float[] { inSize.x, inSize.y };
rotateMatrix.mapPoints(rotatedInSize);
rotatedInSize[0] = Math.abs(rotatedInSize[0]);
rotatedInSize[1] = Math.abs(rotatedInSize[1]);
// ADJUST CROP WIDTH
// Extend the crop all the way to the right, for parallax
// (or all the way to the left, in RTL)
float extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
// Cap the amount of extra width
float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
extraSpace = Math.min(extraSpace, maxExtraSpace);
if (ltr) {
cropRect.right += extraSpace;
} else {
cropRect.left -= extraSpace;
}
// ADJUST CROP HEIGHT
if (isPortrait) {
cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
} else { // LANDSCAPE
float extraPortraitHeight =
defaultWallpaperSize.y / cropScale - cropRect.height();
float expandHeight =
Math.min(Math.min(rotatedInSize[1] - 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(this, uri,
cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
if (onBitmapCroppedHandler != null) {
cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
}
cropTask.execute();
}
public interface OnBitmapCroppedHandler {
public void onBitmapCropped(byte[] imageBytes);
}
protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
Uri mInUri = null;
Context mContext;
String mInFilePath;
byte[] mInImageBytes;
int mInResId = 0;
RectF mCropBounds = null;
int mOutWidth, mOutHeight;
int mRotation;
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 rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mContext = c;
mInFilePath = filePath;
init(cropBounds, rotation,
outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(byte[] imageBytes,
RectF cropBounds, int rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mInImageBytes = imageBytes;
init(cropBounds, rotation,
outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(Context c, Uri inUri,
RectF cropBounds, int rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mContext = c;
mInUri = inUri;
init(cropBounds, rotation,
outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(Context c, Resources res, int inResId,
RectF cropBounds, int rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mContext = c;
mInResId = inResId;
mResources = res;
init(cropBounds, rotation,
outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mCropBounds = cropBounds;
mRotation = rotation;
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 InputStream 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 {
try {
if (mInUri != null) {
return new BufferedInputStream(
mContext.getContentResolver().openInputStream(mInUri));
} else if (mInFilePath != null) {
return mContext.openFileInput(mInFilePath);
} else if (mInImageBytes != null) {
return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
} else {
return new BufferedInputStream(mResources.openRawResource(mInResId));
}
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
}
}
return null;
}
public Point getImageBounds() {
InputStream is = regenerateInputStream();
if (is != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
Utils.closeSilently(is);
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;
WallpaperManager wallpaperManager = null;
if (mSetWallpaper) {
wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
}
if (mSetWallpaper && mNoCrop) {
try {
InputStream is = regenerateInputStream();
if (is != null) {
wallpaperManager.setStream(is);
Utils.closeSilently(is);
}
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
return !failure;
} else {
// Find crop bounds (scaled to original image size)
Rect roundedTrueCrop = new Rect();
Matrix rotateMatrix = new Matrix();
Matrix inverseRotateMatrix = new Matrix();
Point bounds = getImageBounds();
if (mRotation > 0) {
rotateMatrix.setRotate(mRotation);
inverseRotateMatrix.setRotate(-mRotation);
mCropBounds.roundOut(roundedTrueCrop);
mCropBounds = new RectF(roundedTrueCrop);
if (bounds == null) {
Log.w(LOGTAG, "cannot get bounds for image");
failure = true;
return false;
}
float[] rotatedBounds = new float[] { bounds.x, bounds.y };
rotateMatrix.mapPoints(rotatedBounds);
rotatedBounds[0] = Math.abs(rotatedBounds[0]);
rotatedBounds[1] = Math.abs(rotatedBounds[1]);
mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
inverseRotateMatrix.mapRect(mCropBounds);
mCropBounds.offset(bounds.x/2, bounds.y/2);
}
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.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
roundedTrueCrop.height() / mOutHeight));
// Attempt to open a region decoder
BitmapRegionDecoder decoder = null;
InputStream is = null;
try {
is = regenerateInputStream();
if (is == null) {
Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
failure = true;
return false;
}
decoder = BitmapRegionDecoder.newInstance(is, false);
Utils.closeSilently(is);
} catch (IOException e) {
Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
} finally {
Utils.closeSilently(is);
is = null;
}
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
is = regenerateInputStream();
Bitmap fullSize = null;
if (is != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
if (scaleDownSampleSize > 1) {
options.inSampleSize = scaleDownSampleSize;
}
fullSize = BitmapFactory.decodeStream(is, null, options);
Utils.closeSilently(is);
}
if (fullSize != null) {
// Find out the true sample size that was used by the decoder
scaleDownSampleSize = bounds.x / fullSize.getWidth();
mCropBounds.left /= scaleDownSampleSize;
mCropBounds.top /= scaleDownSampleSize;
mCropBounds.bottom /= scaleDownSampleSize;
mCropBounds.right /= scaleDownSampleSize;
mCropBounds.roundOut(roundedTrueCrop);
// Adjust values to account for issues related to rounding
if (roundedTrueCrop.width() > fullSize.getWidth()) {
// Adjust the width
roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
}
if (roundedTrueCrop.right > fullSize.getWidth()) {
// Adjust the left value
int adjustment = roundedTrueCrop.left -
Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
roundedTrueCrop.left -= adjustment;
roundedTrueCrop.right -= adjustment;
}
if (roundedTrueCrop.height() > fullSize.getHeight()) {
// Adjust the height
roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
}
if (roundedTrueCrop.bottom > fullSize.getHeight()) {
// Adjust the top value
int adjustment = roundedTrueCrop.top -
Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
roundedTrueCrop.top -= adjustment;
roundedTrueCrop.bottom -= adjustment;
}
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 || mRotation > 0) {
float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
rotateMatrix.mapPoints(dimsAfter);
dimsAfter[0] = Math.abs(dimsAfter[0]);
dimsAfter[1] = Math.abs(dimsAfter[1]);
if (!(mOutWidth > 0 && mOutHeight > 0)) {
mOutWidth = Math.round(dimsAfter[0]);
mOutHeight = Math.round(dimsAfter[1]);
}
RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
Matrix m = new Matrix();
if (mRotation == 0) {
m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
} else {
Matrix m1 = new Matrix();
m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
Matrix m2 = new Matrix();
m2.setRotate(mRotation);
Matrix m3 = new Matrix();
m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
Matrix m4 = new Matrix();
m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
Matrix c1 = new Matrix();
c1.setConcat(m2, m1);
Matrix c2 = new Matrix();
c2.setConcat(m4, m3);
m.setConcat(c2, c1);
}
Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
(int) returnRect.height(), Bitmap.Config.ARGB_8888);
if (tmp != null) {
Canvas c = new Canvas(tmp);
Paint p = new Paint();
p.setFilterBitmap(true);
c.drawBitmap(crop, m, p);
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 && wallpaperManager != null) {
try {
byte[] outByteArray = tmpOut.toByteArray();
wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
if (mOnBitmapCroppedHandler != null) {
mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
}
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
}
} 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) {
if (mOnEndRunnable != null) {
mOnEndRunnable.run();
}
}
}
protected void updateWallpaperDimensions(int width, int height) {
String spKey = getSharedPreferencesKey();
SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
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 = getDefaultWallpaperSize(res, windowManager);
// 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);
if (savedWidth != wallpaperManager.getDesiredMinimumWidth() ||
savedHeight != wallpaperManager.getDesiredMinimumHeight()) {
wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
}
}
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";
}
}
File diff suppressed because it is too large Load Diff
@@ -1,524 +0,0 @@
/*
* 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.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
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.common.Utils;
import com.android.gallery3d.exif.ExifInterface;
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.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
interface SimpleBitmapRegionDecoder {
int getWidth();
int getHeight();
Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
}
class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
BitmapRegionDecoder mDecoder;
private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
mDecoder = decoder;
}
public static SimpleBitmapRegionDecoderWrapper newInstance(
String pathName, boolean isShareable) {
try {
BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
if (d != null) {
return new SimpleBitmapRegionDecoderWrapper(d);
}
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
return null;
}
return null;
}
public static SimpleBitmapRegionDecoderWrapper newInstance(
InputStream is, boolean isShareable) {
try {
BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
if (d != null) {
return new SimpleBitmapRegionDecoderWrapper(d);
}
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "getting decoder failed", e);
return null;
}
return null;
}
public int getWidth() {
return mDecoder.getWidth();
}
public int getHeight() {
return mDecoder.getHeight();
}
public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
return mDecoder.decodeRegion(wantRegion, options);
}
}
class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
Bitmap mBuffer;
Canvas mTempCanvas;
Paint mTempPaint;
private DumbBitmapRegionDecoder(Bitmap b) {
mBuffer = b;
}
public static DumbBitmapRegionDecoder newInstance(String pathName) {
Bitmap b = BitmapFactory.decodeFile(pathName);
if (b != null) {
return new DumbBitmapRegionDecoder(b);
}
return null;
}
public static DumbBitmapRegionDecoder newInstance(InputStream is) {
Bitmap b = BitmapFactory.decodeStream(is);
if (b != null) {
return new DumbBitmapRegionDecoder(b);
}
return null;
}
public int getWidth() {
return mBuffer.getWidth();
}
public int getHeight() {
return mBuffer.getHeight();
}
public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
if (mTempCanvas == null) {
mTempCanvas = new Canvas();
mTempPaint = new Paint();
mTempPaint.setFilterBitmap(true);
}
int sampleSize = Math.max(options.inSampleSize, 1);
Bitmap newBitmap = Bitmap.createBitmap(
wantRegion.width() / sampleSize,
wantRegion.height() / sampleSize,
Bitmap.Config.ARGB_8888);
mTempCanvas.setBitmap(newBitmap);
mTempCanvas.save();
mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
mTempCanvas.restore();
mTempCanvas.setBitmap(null);
return newBitmap;
}
}
/**
* 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
public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
public static abstract class BitmapSource {
private SimpleBitmapRegionDecoder mDecoder;
private Bitmap mPreview;
private int mPreviewSize;
private int mRotation;
public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
private State mState = State.NOT_LOADED;
public BitmapSource(int previewSize) {
mPreviewSize = previewSize;
}
public boolean loadInBackground() {
ExifInterface ei = new ExifInterface();
if (readExif(ei)) {
Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
if (ori != null) {
mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
}
}
mDecoder = loadBitmapRegionDecoder();
if (mDecoder == null) {
mState = State.ERROR_LOADING;
return false;
} else {
int width = mDecoder.getWidth();
int height = mDecoder.getHeight();
if (mPreviewSize != 0) {
int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
opts.inPreferQualityOverSpeed = true;
float scale = (float) previewSize / Math.max(width, height);
opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
opts.inJustDecodeBounds = false;
mPreview = loadPreviewBitmap(opts);
}
mState = State.LOADED;
return true;
}
}
public State getLoadingState() {
return mState;
}
public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
return mDecoder;
}
public Bitmap getPreviewBitmap() {
return mPreview;
}
public int getPreviewSize() {
return mPreviewSize;
}
public int getRotation() {
return mRotation;
}
public abstract boolean readExif(ExifInterface ei);
public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
}
public static class FilePathBitmapSource extends BitmapSource {
private String mPath;
public FilePathBitmapSource(String path, int previewSize) {
super(previewSize);
mPath = path;
}
@Override
public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
SimpleBitmapRegionDecoder d;
d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
if (d == null) {
d = DumbBitmapRegionDecoder.newInstance(mPath);
}
return d;
}
@Override
public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
return BitmapFactory.decodeFile(mPath, options);
}
@Override
public boolean readExif(ExifInterface ei) {
try {
ei.readExif(mPath);
return true;
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "getting decoder failed", e);
return false;
}
}
}
public static class UriBitmapSource extends BitmapSource {
private Context mContext;
private Uri mUri;
public UriBitmapSource(Context context, Uri uri, int previewSize) {
super(previewSize);
mContext = context;
mUri = uri;
}
private InputStream regenerateInputStream() throws FileNotFoundException {
InputStream is = mContext.getContentResolver().openInputStream(mUri);
return new BufferedInputStream(is);
}
@Override
public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
try {
InputStream is = regenerateInputStream();
SimpleBitmapRegionDecoder regionDecoder =
SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
Utils.closeSilently(is);
if (regionDecoder == null) {
is = regenerateInputStream();
regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
Utils.closeSilently(is);
}
return regionDecoder;
} catch (FileNotFoundException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return null;
} catch (IOException e) {
Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
return null;
}
}
@Override
public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
try {
InputStream is = regenerateInputStream();
Bitmap b = BitmapFactory.decodeStream(is, null, options);
Utils.closeSilently(is);
return b;
} catch (FileNotFoundException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return null;
}
}
@Override
public boolean readExif(ExifInterface ei) {
InputStream is = null;
try {
is = regenerateInputStream();
ei.readExif(is);
Utils.closeSilently(is);
return true;
} catch (FileNotFoundException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} catch (IOException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} finally {
Utils.closeSilently(is);
}
}
}
public static class ResourceBitmapSource extends BitmapSource {
private Resources mRes;
private int mResId;
public ResourceBitmapSource(Resources res, int resId, int previewSize) {
super(previewSize);
mRes = res;
mResId = resId;
}
private InputStream regenerateInputStream() {
InputStream is = mRes.openRawResource(mResId);
return new BufferedInputStream(is);
}
@Override
public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
InputStream is = regenerateInputStream();
SimpleBitmapRegionDecoder regionDecoder =
SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
Utils.closeSilently(is);
if (regionDecoder == null) {
is = regenerateInputStream();
regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
Utils.closeSilently(is);
}
return regionDecoder;
}
@Override
public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
return BitmapFactory.decodeResource(mRes, mResId, options);
}
@Override
public boolean readExif(ExifInterface ei) {
try {
InputStream is = regenerateInputStream();
ei.readExif(is);
Utils.closeSilently(is);
return true;
} catch (IOException e) {
Log.e("BitmapRegionTileSource", "Error reading resource", e);
return false;
}
}
}
SimpleBitmapRegionDecoder 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, BitmapSource source) {
mTileSize = TiledImageRenderer.suggestedTileSize(context);
mRotation = source.getRotation();
mDecoder = source.getBitmapRegionDecoder();
if (mDecoder != null) {
mWidth = mDecoder.getWidth();
mHeight = mDecoder.getHeight();
mOptions = new BitmapFactory.Options();
mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
mOptions.inPreferQualityOverSpeed = true;
mOptions.inTempStorage = new byte[16 * 1024];
int previewSize = source.getPreviewSize();
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(source, 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(BitmapSource source, int targetSize) {
Bitmap result = source.getPreviewBitmap();
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)
float 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;
}
}
@@ -1,438 +0,0 @@
/*
* 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;
}
}
}
}
@@ -1,825 +0,0 @@
/*
* 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
}
}
}
}
@@ -1,386 +0,0 @@
/*
* 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;
public 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;
}
}
}