/*
* Copyright (C) 2012 CyberAgent
*
* 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 jp.co.cyberagent.android.gpuimage;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.media.ExifInterface;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.view.Display;
import android.view.WindowManager;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.Semaphore;
/**
* The main accessor for GPUImage functionality. This class helps to do common
* tasks through a simple interface.
*/
public class GPUImage {
private final Context mContext;
private final GPUImageRenderer mRenderer;
private GLSurfaceView mGlSurfaceView;
private GPUImageFilter mFilter;
private Bitmap mCurrentBitmap;
private ScaleType mScaleType = ScaleType.CENTER_CROP;
/**
* Instantiates a new GPUImage object.
*
* @param context the context
*/
public GPUImage(final Context context) {
if (!supportsOpenGLES2(context)) {
throw new IllegalStateException("OpenGL ES 2.0 is not supported on this phone.");
}
mContext = context;
mFilter = new GPUImageFilter();
mRenderer = new GPUImageRenderer(mFilter);
}
/**
* Checks if OpenGL ES 2.0 is supported on the current device.
*
* @param context the context
* @return true, if successful
*/
private boolean supportsOpenGLES2(final Context context) {
final ActivityManager activityManager = (ActivityManager)
context.getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo =
activityManager.getDeviceConfigurationInfo();
return configurationInfo.reqGlEsVersion >= 0x20000;
}
/**
* Sets the GLSurfaceView which will display the preview.
*
* @param view the GLSurfaceView
*/
public void setGLSurfaceView(final GLSurfaceView view) {
mGlSurfaceView = view;
mGlSurfaceView.setEGLContextClientVersion(2);
mGlSurfaceView.setRenderer(mRenderer);
mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
mGlSurfaceView.requestRender();
}
/**
* Request the preview to be rendered again.
*/
public void requestRender() {
if (mGlSurfaceView != null) {
mGlSurfaceView.requestRender();
}
}
/**
* Sets the up camera to be connected to GPUImage to get a filtered preview.
*
* @param camera the camera
*/
public void setUpCamera(final Camera camera) {
setUpCamera(camera, 0, false, false);
}
/**
* Sets the up camera to be connected to GPUImage to get a filtered preview.
*
* @param camera the camera
* @param degrees by how many degrees the image should be rotated
* @param flipHorizontal if the image should be flipped horizontally
* @param flipVertical if the image should be flipped vertically
*/
public void setUpCamera(final Camera camera, final int degrees, final boolean flipHorizontal,
final boolean flipVertical) {
mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
setUpCameraGingerbread(camera);
} else {
camera.setPreviewCallback(mRenderer);
camera.startPreview();
}
Rotation rotation = Rotation.NORMAL;
switch (degrees) {
case 90:
rotation = Rotation.ROTATION_90;
break;
case 180:
rotation = Rotation.ROTATION_180;
break;
case 270:
rotation = Rotation.ROTATION_270;
break;
}
mRenderer.setRotationCamera(rotation, flipHorizontal, flipVertical);
}
@TargetApi(11)
private void setUpCameraGingerbread(final Camera camera) {
mRenderer.setUpSurfaceTexture(camera);
}
/**
* Sets the filter which should be applied to the image which was (or will
* be) set by setImage(...).
*
* @param filter the new filter
*/
public void setFilter(final GPUImageFilter filter) {
mFilter = filter;
mRenderer.setFilter(mFilter);
requestRender();
}
/**
* Sets the image on which the filter should be applied.
*
* @param bitmap the new image
*/
public void setImage(final Bitmap bitmap) {
setImage(bitmap, false);
mCurrentBitmap = bitmap;
}
private void setImage(final Bitmap bitmap, final boolean recycle) {
mRenderer.setImageBitmap(bitmap, recycle);
requestRender();
}
/**
* This sets the scale type of GPUImage. This has to be run before setting the image.
* If image is set and scale type changed, image needs to be reset.
*
* @param scaleType The new ScaleType
*/
public void setScaleType(ScaleType scaleType) {
mScaleType = scaleType;
mRenderer.setScaleType(scaleType);
mRenderer.deleteImage();
mCurrentBitmap = null;
requestRender();
}
/**
* Deletes the current image.
*/
public void deleteImage() {
mRenderer.deleteImage();
mCurrentBitmap = null;
requestRender();
}
/**
* Sets the image on which the filter should be applied from a Uri.
*
* @param uri the uri of the new image
*/
public void setImage(final Uri uri) {
new LoadImageUriTask(this, uri).execute();
}
/**
* Sets the image on which the filter should be applied from a File.
*
* @param file the file of the new image
*/
public void setImage(final File file) {
new LoadImageFileTask(this, file).execute();
}
private String getPath(final Uri uri) {
String[] projection = {
MediaStore.Images.Media.DATA,
};
Cursor cursor = mContext.getContentResolver()
.query(uri, projection, null, null, null);
int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
String path = null;
if (cursor.moveToFirst()) {
path = cursor.getString(pathIndex);
}
cursor.close();
return path;
}
/**
* Gets the current displayed image with applied filter as a Bitmap.
*
* @return the current image with filter applied
*/
public Bitmap getBitmapWithFilterApplied() {
return getBitmapWithFilterApplied(mCurrentBitmap);
}
/**
* Gets the given bitmap with current filter applied as a Bitmap.
*
* @param bitmap the bitmap on which the current filter should be applied
* @return the bitmap with filter applied
*/
public Bitmap getBitmapWithFilterApplied(final Bitmap bitmap) {
if (mGlSurfaceView != null) {
mRenderer.deleteImage();
final Semaphore lock = new Semaphore(0);
mRenderer.runOnDraw(new Runnable() {
@Override
public void run() {
mFilter.destroy();
lock.release();
}
});
requestRender();
try {
lock.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
GPUImageRenderer renderer = new GPUImageRenderer(mFilter);
renderer.setRotation(Rotation.NORMAL,
mRenderer.isFlippedHorizontally(), mRenderer.isFlippedVertically());
renderer.setScaleType(mScaleType);
PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
buffer.setRenderer(renderer);
renderer.setImageBitmap(bitmap, false);
Bitmap result = buffer.getBitmap();
mFilter.destroy();
renderer.deleteImage();
buffer.destroy();
mRenderer.setFilter(mFilter);
if (mCurrentBitmap != null) {
mRenderer.setImageBitmap(mCurrentBitmap, false);
}
requestRender();
return result;
}
/**
* Gets the images for multiple filters on a image. This can be used to
* quickly get thumbnail images for filters. <br />
* Whenever a new Bitmap is ready, the listener will be called with the
* bitmap. The order of the calls to the listener will be the same as the
* filter order.
*
* @param bitmap the bitmap on which the filters will be applied
* @param filters the filters which will be applied on the bitmap
* @param listener the listener on which the results will be notified
*/
public static void getBitmapForMultipleFilters(final Bitmap bitmap,
final List<GPUImageFilter> filters, final ResponseListener<Bitmap> listener) {
if (filters.isEmpty()) {
return;
}
GPUImageRenderer renderer = new GPUImageRenderer(filters.get(0));
renderer.setImageBitmap(bitmap, false);
PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
buffer.setRenderer(renderer);
for (GPUImageFilter filter : filters) {
renderer.setFilter(filter);
listener.response(buffer.getBitmap());
filter.destroy();
}
renderer.deleteImage();
buffer.destroy();
}
/**
* Save current image with applied filter to Pictures. It will be stored on
* the default Picture folder on the phone below the given folerName and
* fileName. <br />
* This method is async and will notify when the image was saved through the
* listener.
*
* @param folderName the folder name
* @param fileName the file name
* @param listener the listener
*/
public void saveToPictures(final String folderName, final String fileName,
final OnPictureSavedListener listener) {
saveToPictures(mCurrentBitmap, folderName, fileName, listener);
}
/**
* Apply and save the given bitmap with applied filter to Pictures. It will
* be stored on the default Picture folder on the phone below the given
* folerName and fileName. <br />
* This method is async and will notify when the image was saved through the
* listener.
*
* @param bitmap the bitmap
* @param folderName the folder name
* @param fileName the file name
* @param listener the listener
*/
public void saveToPictures(final Bitmap bitmap, final String folderName, final String fileName,
final OnPictureSavedListener listener) {
new SaveTask(bitmap, folderName, fileName, listener).execute();
}
private int getOutputWidth() {
if (mRenderer != null && mRenderer.getFrameWidth() != 0) {
return mRenderer.getFrameWidth();
} else if (mCurrentBitmap != null) {
return mCurrentBitmap.getWidth();
} else {
WindowManager windowManager =
(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
return display.getWidth();
}
}
private int getOutputHeight() {
if (mRenderer != null && mRenderer.getFrameHeight() != 0) {
return mRenderer.getFrameHeight();
} else if (mCurrentBitmap != null) {
return mCurrentBitmap.getHeight();
} else {
WindowManager windowManager =
(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
return display.getHeight();
}
}
private class SaveTask extends AsyncTask<Void, Void, Void> {
private final Bitmap mBitmap;
private final String mFolderName;
private final String mFileName;
private final OnPictureSavedListener mListener;
private final Handler mHandler;
public SaveTask(final Bitmap bitmap, final String folderName, final String fileName,
final OnPictureSavedListener listener) {
mBitmap = bitmap;
mFolderName = folderName;
mFileName = fileName;
mListener = listener;
mHandler = new Handler();
}
@Override
protected Void doInBackground(final Void... params) {
Bitmap result = getBitmapWithFilterApplied(mBitmap);
saveImage(mFolderName, mFileName, result);
return null;
}
private void saveImage(final String folderName, final String fileName, final Bitmap image) {
File path = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File file = new File(path, folderName + "/" + fileName);
try {
file.getParentFile().mkdirs();
image.compress(CompressFormat.JPEG, 80, new FileOutputStream(file));
MediaScannerConnection.scanFile(mContext,
new String[] {
file.toString()
}, null,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(final String path, final Uri uri) {
if (mListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mListener.onPictureSaved(uri);
}
});
}
}
});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public interface OnPictureSavedListener {
void onPictureSaved(Uri uri);
}
private class LoadImageUriTask extends LoadImageTask {
private final Uri mUri;
public LoadImageUriTask(GPUImage gpuImage, Uri uri) {
super(gpuImage);
mUri = uri;
}
@Override
protected Bitmap decode(BitmapFactory.Options options) {
try {
InputStream inputStream;
if (mUri.getScheme().startsWith("http") || mUri.getScheme().startsWith("https")) {
inputStream = new URL(mUri.toString()).openStream();
} else {
inputStream = mContext.getContentResolver().openInputStream(mUri);
}
return BitmapFactory.decodeStream(inputStream, null, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected int getImageOrientation() throws IOException {
Cursor cursor = mContext.getContentResolver().query(mUri,
new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null);
if (cursor == null || cursor.getCount() != 1) {
return 0;
}
cursor.moveToFirst();
return cursor.getInt(0);
}
}
private class LoadImageFileTask extends LoadImageTask {
private final File mImageFile;
public LoadImageFileTask(GPUImage gpuImage, File file) {
super(gpuImage);
mImageFile = file;
}
@Override
protected Bitmap decode(BitmapFactory.Options options) {
return BitmapFactory.decodeFile(mImageFile.getAbsolutePath(), options);
}
@Override
protected int getImageOrientation() throws IOException {
ExifInterface exif = new ExifInterface(mImageFile.getAbsolutePath());
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
switch (orientation) {
case ExifInterface.ORIENTATION_NORMAL:
return 0;
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
}
}
private abstract class LoadImageTask extends AsyncTask<Void, Void, Bitmap> {
private final GPUImage mGPUImage;
private int mOutputWidth;
private int mOutputHeight;
@SuppressWarnings("deprecation")
public LoadImageTask(final GPUImage gpuImage) {
mGPUImage = gpuImage;
}
@Override
protected Bitmap doInBackground(Void... params) {
if (mRenderer != null && mRenderer.getFrameWidth() == 0) {
try {
synchronized (mRenderer.mSurfaceChangedWaiter) {
mRenderer.mSurfaceChangedWaiter.wait(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mOutputWidth = getOutputWidth();
mOutputHeight = getOutputHeight();
return loadResizedImage();
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
mGPUImage.setImage(bitmap);
}
protected abstract Bitmap decode(BitmapFactory.Options options);
private Bitmap loadResizedImage() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
decode(options);
int scale = 1;
while (checkSize(options.outWidth / scale > mOutputWidth, options.outHeight / scale > mOutputHeight)) {
scale++;
}
scale--;
if (scale < 1) {
scale = 1;
}
options = new BitmapFactory.Options();
options.inSampleSize = scale;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inTempStorage = new byte[32 * 1024];
Bitmap bitmap = decode(options);
if (bitmap == null) {
return null;
}
bitmap = rotateImage(bitmap);
bitmap = scaleBitmap(bitmap);
return bitmap;
}
private Bitmap scaleBitmap(Bitmap bitmap) {
// resize to desired dimensions
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] newSize = getScaleSize(width, height);
Bitmap workBitmap = Bitmap.createScaledBitmap(bitmap, newSize[0], newSize[1], true);
bitmap.recycle();
bitmap = workBitmap;
System.gc();
if (mScaleType == ScaleType.CENTER_CROP) {
// Crop it
int diffWidth = newSize[0] - mOutputWidth;
int diffHeight = newSize[1] - mOutputHeight;
workBitmap = Bitmap.createBitmap(bitmap, diffWidth / 2, diffHeight / 2,
newSize[0] - diffWidth, newSize[1] - diffHeight);
bitmap.recycle();
bitmap = workBitmap;
}
return bitmap;
}
/**
* Retrieve the scaling size for the image dependent on the ScaleType.<br />
* <br/>
* If CROP: sides are same size or bigger than output's sides<br />
* Else : sides are same size or smaller than output's sides
*/
private int[] getScaleSize(int width, int height) {
float newWidth;
float newHeight;
float withRatio = (float) width / mOutputWidth;
float heightRatio = (float) height / mOutputHeight;
boolean adjustWidth = mScaleType == ScaleType.CENTER_CROP
? withRatio > heightRatio : withRatio < heightRatio;
if (adjustWidth) {
newHeight = mOutputHeight;
newWidth = (newHeight / height) * width;
} else {
newWidth = mOutputWidth;
newHeight = (newWidth / width) * height;
}
return new int[]{Math.round(newWidth), Math.round(newHeight)};
}
private boolean checkSize(boolean widthBigger, boolean heightBigger) {
if (mScaleType == ScaleType.CENTER_CROP) {
return widthBigger && heightBigger;
} else {
return widthBigger || heightBigger;
}
}
private Bitmap rotateImage(final Bitmap bitmap) {
if (bitmap == null) {
return null;
}
Bitmap rotatedBitmap = bitmap;
try {
int orientation = getImageOrientation();
if (orientation != 0) {
Matrix matrix = new Matrix();
matrix.postRotate(orientation);
rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
bitmap.recycle();
}
} catch (IOException e) {
e.printStackTrace();
}
return rotatedBitmap;
}
protected abstract int getImageOrientation() throws IOException;
}
public interface ResponseListener<T> {
void response(T item);
}
public enum ScaleType { CENTER_INSIDE, CENTER_CROP }
}