1 /** 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.gallery3d.common; 17 18 import android.app.WallpaperManager; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Bitmap.CompressFormat; 23 import android.graphics.BitmapFactory; 24 import android.graphics.BitmapRegionDecoder; 25 import android.graphics.Canvas; 26 import android.graphics.Matrix; 27 import android.graphics.Paint; 28 import android.graphics.Point; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.util.Log; 34 35 import java.io.BufferedInputStream; 36 import java.io.ByteArrayInputStream; 37 import java.io.ByteArrayOutputStream; 38 import java.io.FileNotFoundException; 39 import java.io.IOException; 40 import java.io.InputStream; 41 42 public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> { 43 44 public interface OnBitmapCroppedHandler { onBitmapCropped(byte[] imageBytes)45 public void onBitmapCropped(byte[] imageBytes); 46 } 47 48 private static final int DEFAULT_COMPRESS_QUALITY = 90; 49 private static final String LOGTAG = "BitmapCropTask"; 50 51 Uri mInUri = null; 52 Context mContext; 53 String mInFilePath; 54 byte[] mInImageBytes; 55 int mInResId = 0; 56 RectF mCropBounds = null; 57 int mOutWidth, mOutHeight; 58 int mRotation; 59 boolean mSetWallpaper; 60 boolean mSaveCroppedBitmap; 61 Bitmap mCroppedBitmap; 62 Runnable mOnEndRunnable; 63 Resources mResources; 64 BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler; 65 boolean mNoCrop; 66 BitmapCropTask(Context c, String filePath, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable)67 public BitmapCropTask(Context c, String filePath, 68 RectF cropBounds, int rotation, int outWidth, int outHeight, 69 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 70 mContext = c; 71 mInFilePath = filePath; 72 init(cropBounds, rotation, 73 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 74 } 75 BitmapCropTask(byte[] imageBytes, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable)76 public BitmapCropTask(byte[] imageBytes, 77 RectF cropBounds, int rotation, int outWidth, int outHeight, 78 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 79 mInImageBytes = imageBytes; 80 init(cropBounds, rotation, 81 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 82 } 83 BitmapCropTask(Context c, Uri inUri, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable)84 public BitmapCropTask(Context c, Uri inUri, 85 RectF cropBounds, int rotation, int outWidth, int outHeight, 86 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 87 mContext = c; 88 mInUri = inUri; 89 init(cropBounds, rotation, 90 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 91 } 92 BitmapCropTask(Context c, Resources res, int inResId, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable)93 public BitmapCropTask(Context c, Resources res, int inResId, 94 RectF cropBounds, int rotation, int outWidth, int outHeight, 95 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 96 mContext = c; 97 mInResId = inResId; 98 mResources = res; 99 init(cropBounds, rotation, 100 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); 101 } 102 init(RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable)103 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, 104 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { 105 mCropBounds = cropBounds; 106 mRotation = rotation; 107 mOutWidth = outWidth; 108 mOutHeight = outHeight; 109 mSetWallpaper = setWallpaper; 110 mSaveCroppedBitmap = saveCroppedBitmap; 111 mOnEndRunnable = onEndRunnable; 112 } 113 setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler)114 public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { 115 mOnBitmapCroppedHandler = handler; 116 } 117 setNoCrop(boolean value)118 public void setNoCrop(boolean value) { 119 mNoCrop = value; 120 } 121 setOnEndRunnable(Runnable onEndRunnable)122 public void setOnEndRunnable(Runnable onEndRunnable) { 123 mOnEndRunnable = onEndRunnable; 124 } 125 126 // Helper to setup input stream regenerateInputStream()127 private InputStream regenerateInputStream() { 128 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { 129 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + 130 "image byte array given"); 131 } else { 132 try { 133 if (mInUri != null) { 134 return new BufferedInputStream( 135 mContext.getContentResolver().openInputStream(mInUri)); 136 } else if (mInFilePath != null) { 137 return mContext.openFileInput(mInFilePath); 138 } else if (mInImageBytes != null) { 139 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); 140 } else { 141 return new BufferedInputStream(mResources.openRawResource(mInResId)); 142 } 143 } catch (FileNotFoundException e) { 144 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); 145 } 146 } 147 return null; 148 } 149 getImageBounds()150 public Point getImageBounds() { 151 InputStream is = regenerateInputStream(); 152 if (is != null) { 153 BitmapFactory.Options options = new BitmapFactory.Options(); 154 options.inJustDecodeBounds = true; 155 BitmapFactory.decodeStream(is, null, options); 156 Utils.closeSilently(is); 157 if (options.outWidth != 0 && options.outHeight != 0) { 158 return new Point(options.outWidth, options.outHeight); 159 } 160 } 161 return null; 162 } 163 setCropBounds(RectF cropBounds)164 public void setCropBounds(RectF cropBounds) { 165 mCropBounds = cropBounds; 166 } 167 getCroppedBitmap()168 public Bitmap getCroppedBitmap() { 169 return mCroppedBitmap; 170 } cropBitmap()171 public boolean cropBitmap() { 172 boolean failure = false; 173 174 175 WallpaperManager wallpaperManager = null; 176 if (mSetWallpaper) { 177 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); 178 } 179 180 181 if (mSetWallpaper && mNoCrop) { 182 try { 183 InputStream is = regenerateInputStream(); 184 if (is != null) { 185 wallpaperManager.setStream(is); 186 Utils.closeSilently(is); 187 } 188 } catch (IOException e) { 189 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 190 failure = true; 191 } 192 return !failure; 193 } else { 194 // Find crop bounds (scaled to original image size) 195 Rect roundedTrueCrop = new Rect(); 196 Matrix rotateMatrix = new Matrix(); 197 Matrix inverseRotateMatrix = new Matrix(); 198 199 Point bounds = getImageBounds(); 200 if (mRotation > 0) { 201 rotateMatrix.setRotate(mRotation); 202 inverseRotateMatrix.setRotate(-mRotation); 203 204 mCropBounds.roundOut(roundedTrueCrop); 205 mCropBounds = new RectF(roundedTrueCrop); 206 207 if (bounds == null) { 208 Log.w(LOGTAG, "cannot get bounds for image"); 209 failure = true; 210 return false; 211 } 212 213 float[] rotatedBounds = new float[] { bounds.x, bounds.y }; 214 rotateMatrix.mapPoints(rotatedBounds); 215 rotatedBounds[0] = Math.abs(rotatedBounds[0]); 216 rotatedBounds[1] = Math.abs(rotatedBounds[1]); 217 218 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); 219 inverseRotateMatrix.mapRect(mCropBounds); 220 mCropBounds.offset(bounds.x/2, bounds.y/2); 221 222 } 223 224 mCropBounds.roundOut(roundedTrueCrop); 225 226 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { 227 Log.w(LOGTAG, "crop has bad values for full size image"); 228 failure = true; 229 return false; 230 } 231 232 // See how much we're reducing the size of the image 233 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, 234 roundedTrueCrop.height() / mOutHeight)); 235 // Attempt to open a region decoder 236 BitmapRegionDecoder decoder = null; 237 InputStream is = null; 238 try { 239 is = regenerateInputStream(); 240 if (is == null) { 241 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); 242 failure = true; 243 return false; 244 } 245 decoder = BitmapRegionDecoder.newInstance(is, false); 246 Utils.closeSilently(is); 247 } catch (IOException e) { 248 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); 249 } finally { 250 Utils.closeSilently(is); 251 is = null; 252 } 253 254 Bitmap crop = null; 255 if (decoder != null) { 256 // Do region decoding to get crop bitmap 257 BitmapFactory.Options options = new BitmapFactory.Options(); 258 if (scaleDownSampleSize > 1) { 259 options.inSampleSize = scaleDownSampleSize; 260 } 261 crop = decoder.decodeRegion(roundedTrueCrop, options); 262 decoder.recycle(); 263 } 264 265 if (crop == null) { 266 // BitmapRegionDecoder has failed, try to crop in-memory 267 is = regenerateInputStream(); 268 Bitmap fullSize = null; 269 if (is != null) { 270 BitmapFactory.Options options = new BitmapFactory.Options(); 271 if (scaleDownSampleSize > 1) { 272 options.inSampleSize = scaleDownSampleSize; 273 } 274 fullSize = BitmapFactory.decodeStream(is, null, options); 275 Utils.closeSilently(is); 276 } 277 if (fullSize != null) { 278 // Find out the true sample size that was used by the decoder 279 scaleDownSampleSize = bounds.x / fullSize.getWidth(); 280 mCropBounds.left /= scaleDownSampleSize; 281 mCropBounds.top /= scaleDownSampleSize; 282 mCropBounds.bottom /= scaleDownSampleSize; 283 mCropBounds.right /= scaleDownSampleSize; 284 mCropBounds.roundOut(roundedTrueCrop); 285 286 // Adjust values to account for issues related to rounding 287 if (roundedTrueCrop.width() > fullSize.getWidth()) { 288 // Adjust the width 289 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); 290 } 291 if (roundedTrueCrop.right > fullSize.getWidth()) { 292 // Adjust the left value 293 int adjustment = roundedTrueCrop.left - 294 Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); 295 roundedTrueCrop.left -= adjustment; 296 roundedTrueCrop.right -= adjustment; 297 } 298 if (roundedTrueCrop.height() > fullSize.getHeight()) { 299 // Adjust the height 300 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); 301 } 302 if (roundedTrueCrop.bottom > fullSize.getHeight()) { 303 // Adjust the top value 304 int adjustment = roundedTrueCrop.top - 305 Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); 306 roundedTrueCrop.top -= adjustment; 307 roundedTrueCrop.bottom -= adjustment; 308 } 309 310 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, 311 roundedTrueCrop.top, roundedTrueCrop.width(), 312 roundedTrueCrop.height()); 313 } 314 } 315 316 if (crop == null) { 317 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); 318 failure = true; 319 return false; 320 } 321 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { 322 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; 323 rotateMatrix.mapPoints(dimsAfter); 324 dimsAfter[0] = Math.abs(dimsAfter[0]); 325 dimsAfter[1] = Math.abs(dimsAfter[1]); 326 327 if (!(mOutWidth > 0 && mOutHeight > 0)) { 328 mOutWidth = Math.round(dimsAfter[0]); 329 mOutHeight = Math.round(dimsAfter[1]); 330 } 331 332 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); 333 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); 334 335 Matrix m = new Matrix(); 336 if (mRotation == 0) { 337 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 338 } else { 339 Matrix m1 = new Matrix(); 340 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); 341 Matrix m2 = new Matrix(); 342 m2.setRotate(mRotation); 343 Matrix m3 = new Matrix(); 344 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); 345 Matrix m4 = new Matrix(); 346 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 347 348 Matrix c1 = new Matrix(); 349 c1.setConcat(m2, m1); 350 Matrix c2 = new Matrix(); 351 c2.setConcat(m4, m3); 352 m.setConcat(c2, c1); 353 } 354 355 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), 356 (int) returnRect.height(), Bitmap.Config.ARGB_8888); 357 if (tmp != null) { 358 Canvas c = new Canvas(tmp); 359 Paint p = new Paint(); 360 p.setFilterBitmap(true); 361 c.drawBitmap(crop, m, p); 362 crop = tmp; 363 } 364 } 365 366 if (mSaveCroppedBitmap) { 367 mCroppedBitmap = crop; 368 } 369 370 // Compress to byte array 371 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); 372 if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) { 373 // If we need to set to the wallpaper, set it 374 if (mSetWallpaper && wallpaperManager != null) { 375 try { 376 byte[] outByteArray = tmpOut.toByteArray(); 377 wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); 378 if (mOnBitmapCroppedHandler != null) { 379 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); 380 } 381 } catch (IOException e) { 382 Log.w(LOGTAG, "cannot write stream to wallpaper", e); 383 failure = true; 384 } 385 } 386 } else { 387 Log.w(LOGTAG, "cannot compress bitmap"); 388 failure = true; 389 } 390 } 391 return !failure; // True if any of the operations failed 392 } 393 394 @Override doInBackground(Void... params)395 protected Boolean doInBackground(Void... params) { 396 return cropBitmap(); 397 } 398 399 @Override onPostExecute(Boolean result)400 protected void onPostExecute(Boolean result) { 401 if (mOnEndRunnable != null) { 402 mOnEndRunnable.run(); 403 } 404 } 405 }