/* * 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.graphics.PixelFormat; 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.provider.MediaStore.MediaColumns; import android.view.Display; import android.view.WindowManager; import java.io.*; import java.net.URL; import java.util.List; /** * 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.setEGLConfigChooser(8, 8, 8, 8, 16, 0); mGlSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888); 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) { mCurrentBitmap = bitmap; mRenderer.setImageBitmap(bitmap, false); 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(); } /** * Sets the rotation of the displayed image. * * @param rotation new rotation */ public void setRotation(Rotation rotation) { mRenderer.setRotation(rotation); } /** * 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 = { MediaColumns.DATA, }; Cursor cursor = mContext.getContentResolver() .query(uri, projection, null, null, null); int pathIndex = cursor.getColumnIndexOrThrow(MediaColumns.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(); mRenderer.runOnDraw(new Runnable() { @Override public void run() { synchronized(mFilter) { mFilter.destroy(); mFilter.notify(); } } }); synchronized(mFilter) { requestRender(); try { mFilter.wait(); } 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(); } /** * Deprecated: Please use * {@link GPUImageView#saveToPictures(String, String, jp.co.cyberagent.android.gpuimage.GPUImageView.OnPictureSavedListener)} * * Save current image with applied filter to Pictures. It will be stored on * the default Picture folder on the phone below the given folderName 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 */ @Deprecated public void saveToPictures(final String folderName, final String fileName, final OnPictureSavedListener listener) { saveToPictures(mCurrentBitmap, folderName, fileName, listener); } /** * Deprecated: Please use * {@link GPUImageView#saveToPictures(String, String, jp.co.cyberagent.android.gpuimage.GPUImageView.OnPictureSavedListener)} * * 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 */ @Deprecated public void saveToPictures(final Bitmap bitmap, final String folderName, final String fileName, final OnPictureSavedListener listener) { new SaveTask(bitmap, folderName, fileName, listener).execute(); } /** * Runs the given Runnable on the OpenGL thread. * * @param runnable The runnable to be run on the OpenGL thread. */ public void runOnGLThread(Runnable runnable) { mRenderer.runOnDrawEnd(runnable); } 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(); } } @Deprecated 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, 100, 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(); int orientation = cursor.getInt(0); cursor.close(); return orientation; } } 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.deleteImage(); 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); if (workBitmap != bitmap) { 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); if (workBitmap != bitmap) { 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 } }