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 { 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 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 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 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 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 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 114 public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { 115 mOnBitmapCroppedHandler = handler; 116 } 117 118 public void setNoCrop(boolean value) { 119 mNoCrop = value; 120 } 121 122 public void setOnEndRunnable(Runnable onEndRunnable) { 123 mOnEndRunnable = onEndRunnable; 124 } 125 126 // Helper to setup input stream 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 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 164 public void setCropBounds(RectF cropBounds) { 165 mCropBounds = cropBounds; 166 } 167 168 public Bitmap getCroppedBitmap() { 169 return mCroppedBitmap; 170 } 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 395 protected Boolean doInBackground(Void... params) { 396 return cropBitmap(); 397 } 398 399 @Override 400 protected void onPostExecute(Boolean result) { 401 if (mOnEndRunnable != null) { 402 mOnEndRunnable.run(); 403 } 404 } 405 }