1 /* 2 * Copyright (C) 2009 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 17 package com.android.videoeditor.util; 18 19 import java.io.File; 20 import java.io.FileNotFoundException; 21 import java.io.FileOutputStream; 22 import java.io.IOException; 23 import java.lang.Math; 24 25 import android.content.Context; 26 import android.graphics.Bitmap; 27 import android.graphics.Bitmap.CompressFormat; 28 import android.graphics.BitmapFactory; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.Matrix; 33 import android.graphics.Paint; 34 import android.graphics.Rect; 35 import android.graphics.Typeface; 36 import android.media.ExifInterface; 37 import android.util.Log; 38 39 import com.android.videoeditor.R; 40 import com.android.videoeditor.service.MovieOverlay; 41 42 /** 43 * Image utility methods 44 */ 45 public class ImageUtils { 46 /** 47 * Logging 48 */ 49 private static final String TAG = "ImageUtils"; 50 51 // The resize paint 52 private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG); 53 54 // The match aspect ratio mode for scaleImage 55 public static int MATCH_SMALLER_DIMENSION = 1; 56 public static int MATCH_LARGER_DIMENSION = 2; 57 58 /** 59 * It is not possible to instantiate this class 60 */ 61 private ImageUtils() { 62 } 63 64 /** 65 * Resize a bitmap to the specified width and height. 66 * 67 * @param filename The filename 68 * @param width The thumbnail width 69 * @param height The thumbnail height 70 * @param match MATCH_SMALLER_DIMENSION or MATCH_LARGER_DIMMENSION 71 * 72 * @return The resized bitmap 73 */ 74 public static Bitmap scaleImage(String filename, int width, int height, int match) 75 throws IOException { 76 final BitmapFactory.Options dbo = new BitmapFactory.Options(); 77 dbo.inJustDecodeBounds = true; 78 BitmapFactory.decodeFile(filename, dbo); 79 80 final int nativeWidth = dbo.outWidth; 81 final int nativeHeight = dbo.outHeight; 82 83 final Bitmap srcBitmap; 84 float scaledWidth, scaledHeight; 85 final BitmapFactory.Options options = new BitmapFactory.Options(); 86 if (nativeWidth > width || nativeHeight > height) { 87 float dx = ((float) nativeWidth) / ((float) width); 88 float dy = ((float) nativeHeight) / ((float) height); 89 float scale = (match == MATCH_SMALLER_DIMENSION) ? Math.max(dx,dy) : Math.min(dx,dy); 90 scaledWidth = nativeWidth / scale; 91 scaledHeight = nativeHeight / scale; 92 // Create the bitmap from file. 93 options.inSampleSize = (scale > 1.0f) ? ((int) scale) : 1; 94 } else { 95 scaledWidth = width; 96 scaledHeight = height; 97 options.inSampleSize = 1; 98 } 99 100 srcBitmap = BitmapFactory.decodeFile(filename, options); 101 if (srcBitmap == null) { 102 throw new IOException("Cannot decode file: " + filename); 103 } 104 105 // Create the canvas bitmap. 106 final Bitmap bitmap = Bitmap.createBitmap(Math.round(scaledWidth), 107 Math.round(scaledHeight), 108 Bitmap.Config.ARGB_8888); 109 final Canvas canvas = new Canvas(bitmap); 110 canvas.drawBitmap(srcBitmap, 111 new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight()), 112 new Rect(0, 0, Math.round(scaledWidth), Math.round(scaledHeight)), 113 sResizePaint); 114 115 // Release the source bitmap 116 srcBitmap.recycle(); 117 return bitmap; 118 } 119 120 /** 121 * Rotate a JPEG according to the EXIF data 122 * 123 * @param inputFilename The name of the input file (must be a JPEG filename) 124 * @param outputFile The rotated file 125 * 126 * @return true if the image was rotated 127 */ 128 public static boolean transformJpeg(String inputFilename, File outputFile) 129 throws IOException { 130 final ExifInterface exif = new ExifInterface(inputFilename); 131 final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 132 ExifInterface.ORIENTATION_UNDEFINED); 133 134 if (Log.isLoggable(TAG, Log.DEBUG)) { 135 Log.d(TAG, "Exif orientation: " + orientation); 136 } 137 138 // Degrees by which we rotate the image. 139 int degrees = 0; 140 switch (orientation) { 141 case ExifInterface.ORIENTATION_ROTATE_90: { 142 degrees = 90; 143 break; 144 } 145 146 case ExifInterface.ORIENTATION_ROTATE_180: { 147 degrees = 180; 148 break; 149 } 150 151 case ExifInterface.ORIENTATION_ROTATE_270: { 152 degrees = 270; 153 break; 154 } 155 } 156 rotateAndScaleImage(inputFilename, degrees, outputFile); 157 return degrees != 0; 158 } 159 160 /** 161 * Rotates an image according to the specified {@code orientation}. 162 * We limit the number of pixels of the scaled image. Thus the image 163 * will typically be downsampled. 164 * 165 * @param inputFilename The input filename 166 * @param orientation The rotation angle 167 * @param outputFile The output file 168 */ 169 private static void rotateAndScaleImage(String inputFilename, int orientation, File outputFile) 170 throws FileNotFoundException, IOException { 171 // In order to avoid OutOfMemoryError when rotating the image, we scale down the size of the 172 // input image. We set the maxmimum number of allowed pixels to 2M and scale down the image 173 // accordingly. 174 175 // Determine width and height of the original bitmap without allocating memory for it, 176 BitmapFactory.Options opt = new BitmapFactory.Options(); 177 opt.inJustDecodeBounds = true; 178 BitmapFactory.decodeFile(inputFilename, opt); 179 180 // Determine the scale factor based on the ratio of pixel count over max allowed pixels. 181 final int width = opt.outWidth; 182 final int height = opt.outHeight; 183 final int pixelCount = width * height; 184 final int MAX_PIXELS_FOR_SCALED_IMAGE = 2000000; 185 double scale = Math.sqrt( (double) pixelCount / MAX_PIXELS_FOR_SCALED_IMAGE); 186 if (scale <= 1) { 187 scale = 1; 188 } else { 189 // Make the scale factor a power of 2 for faster processing. Also the resulting bitmap may 190 // have different dimensions than what has been requested if the scale factor is not a 191 // power of 2. 192 scale = nextPowerOf2((int) Math.ceil(scale)); 193 } 194 195 // Load the scaled image. 196 BitmapFactory.Options opt2 = new BitmapFactory.Options(); 197 opt2.inSampleSize = (int) scale; 198 final Bitmap scaledBmp = BitmapFactory.decodeFile(inputFilename, opt2); 199 200 // Rotation matrix used to rotate the image. 201 final Matrix mtx = new Matrix(); 202 mtx.postRotate(orientation); 203 204 final Bitmap rotatedBmp = Bitmap.createBitmap(scaledBmp, 0, 0, 205 scaledBmp.getWidth(), scaledBmp.getHeight(), mtx, true); 206 scaledBmp.recycle(); 207 208 // Save the rotated image to a file in the current project folder 209 final FileOutputStream fos = new FileOutputStream(outputFile); 210 rotatedBmp.compress(CompressFormat.JPEG, 100, fos); 211 fos.close(); 212 213 rotatedBmp.recycle(); 214 } 215 216 /** 217 * Returns the next power of two. 218 * Returns the input if it is already power of 2. 219 * Throws IllegalArgumentException if the input is <= 0 or the answer overflows. 220 */ 221 private static int nextPowerOf2(int n) { 222 if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException(); 223 n -= 1; 224 n |= n >> 16; 225 n |= n >> 8; 226 n |= n >> 4; 227 n |= n >> 2; 228 n |= n >> 1; 229 return n + 1; 230 } 231 232 /** 233 * Build an overlay image 234 * 235 * @param context The context 236 * @param inputBitmap If the bitmap is provided no not create a new one 237 * @param overlayType The overlay type 238 * @param title The title 239 * @param subTitle The subtitle 240 * @param width The width 241 * @param height The height 242 * 243 * @return The bitmap 244 */ 245 public static Bitmap buildOverlayBitmap(Context context, Bitmap inputBitmap, int overlayType, 246 String title, String subTitle, int width, int height) { 247 final Bitmap overlayBitmap; 248 if (inputBitmap == null) { 249 overlayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 250 } else { 251 overlayBitmap = inputBitmap; 252 } 253 254 overlayBitmap.eraseColor(Color.TRANSPARENT); 255 final Canvas canvas = new Canvas(overlayBitmap); 256 257 switch (overlayType) { 258 case MovieOverlay.OVERLAY_TYPE_CENTER_1: { 259 drawCenterOverlay(context, canvas, R.drawable.overlay_background_1, 260 Color.WHITE, title, subTitle, width, height); 261 break; 262 } 263 264 case MovieOverlay.OVERLAY_TYPE_BOTTOM_1: { 265 drawBottomOverlay(context, canvas, R.drawable.overlay_background_1, 266 Color.WHITE, title, subTitle, width, height); 267 break; 268 } 269 270 case MovieOverlay.OVERLAY_TYPE_CENTER_2: { 271 drawCenterOverlay(context, canvas, R.drawable.overlay_background_2, 272 Color.BLACK, title, subTitle, width, height); 273 break; 274 } 275 276 case MovieOverlay.OVERLAY_TYPE_BOTTOM_2: { 277 drawBottomOverlay(context, canvas, R.drawable.overlay_background_2, 278 Color.BLACK, title, subTitle, width, height); 279 break; 280 } 281 282 default: { 283 throw new IllegalArgumentException("Unsupported overlay type: " + overlayType); 284 } 285 } 286 287 return overlayBitmap; 288 } 289 290 /** 291 * Build an overlay image in the center third of the image 292 * 293 * @param context The context 294 * @param canvas The canvas 295 * @param drawableId The overlay background drawable if 296 * @param textColor The text color 297 * @param title The title 298 * @param subTitle The subtitle 299 * @param width The width 300 * @param height The height 301 */ 302 private static void drawCenterOverlay(Context context, Canvas canvas, int drawableId, 303 int textColor, String title, String subTitle, int width, int height) { 304 final int INSET = width / 72; 305 final int startHeight = (height / 3) + INSET; 306 final Drawable background = context.getResources().getDrawable(drawableId); 307 background.setBounds(INSET, startHeight, width - INSET, 308 ((2 * height) / 3) - INSET); 309 background.draw(canvas); 310 311 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 312 p.setTypeface(Typeface.DEFAULT_BOLD); 313 p.setColor(textColor); 314 315 final int titleFontSize = height / 12; 316 final int maxWidth = width - (2 * INSET) - (2 * titleFontSize); 317 final int startYOffset = startHeight + (height / 6); 318 if (title != null) { 319 p.setTextSize(titleFontSize); 320 title = StringUtils.trimText(title, p, maxWidth); 321 canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2, 322 startYOffset - p.descent(), p); 323 } 324 325 if (subTitle != null) { 326 p.setTextSize(titleFontSize - 6); 327 subTitle = StringUtils.trimText(subTitle, p, maxWidth); 328 canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2, 329 startYOffset - p.ascent(), p); 330 } 331 } 332 333 /** 334 * Build an overlay image in the lower third of the image 335 * 336 * @param context The context 337 * @param canvas The canvas 338 * @param drawableId The overlay background drawable if 339 * @param textColor The text color 340 * @param title The title 341 * @param subTitle The subtitle 342 * @param width The width 343 * @param height The height 344 */ 345 private static void drawBottomOverlay(Context context, Canvas canvas, int drawableId, 346 int textColor, String title, String subTitle, int width, int height) { 347 final int INSET = width / 72; 348 final int startHeight = ((2 * height) / 3) + INSET; 349 final Drawable background = context.getResources().getDrawable(drawableId); 350 background.setBounds(INSET, startHeight, width - INSET, height - INSET); 351 background.draw(canvas); 352 353 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 354 p.setTypeface(Typeface.DEFAULT_BOLD); 355 p.setColor(textColor); 356 357 final int titleFontSize = height / 12; 358 final int maxWidth = width - (2 * INSET) - (2 * titleFontSize); 359 final int startYOffset = startHeight + (height / 6); 360 if (title != null) { 361 p.setTextSize(titleFontSize); 362 title = StringUtils.trimText(title, p, maxWidth); 363 canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2, 364 startYOffset - p.descent(), p); 365 } 366 367 if (subTitle != null) { 368 p.setTextSize(titleFontSize - 6); 369 subTitle = StringUtils.trimText(subTitle, p, maxWidth); 370 canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2, 371 startYOffset - p.ascent(), p); 372 } 373 } 374 375 /** 376 * Build an overlay preview image 377 * 378 * @param context The context 379 * @param canvas The canvas 380 * @param overlayType The overlay type 381 * @param title The title 382 * @param subTitle The subtitle 383 * @param startX The start horizontal position 384 * @param startY The start vertical position 385 * @param width The width 386 * @param height The height 387 */ 388 public static void buildOverlayPreview(Context context, Canvas canvas, int overlayType, 389 String title, String subTitle, int startX, int startY, int width, int height) { 390 switch (overlayType) { 391 case MovieOverlay.OVERLAY_TYPE_CENTER_1: 392 case MovieOverlay.OVERLAY_TYPE_BOTTOM_1: { 393 drawOverlayPreview(context, canvas, R.drawable.overlay_background_1, 394 Color.WHITE, title, subTitle, startX, startY, width, height); 395 break; 396 } 397 398 case MovieOverlay.OVERLAY_TYPE_CENTER_2: 399 case MovieOverlay.OVERLAY_TYPE_BOTTOM_2: { 400 drawOverlayPreview(context, canvas, R.drawable.overlay_background_2, 401 Color.BLACK, title, subTitle, startX, startY, width, height); 402 break; 403 } 404 405 default: { 406 throw new IllegalArgumentException("Unsupported overlay type: " + overlayType); 407 } 408 } 409 } 410 411 /** 412 * Build an overlay image in the lower third of the image 413 * 414 * @param context The context 415 * @param canvas The canvas 416 * @param drawableId The overlay background drawable if 417 * @param title The title 418 * @param subTitle The subtitle 419 * @param width The width 420 * @param height The height 421 */ 422 private static void drawOverlayPreview(Context context, Canvas canvas, int drawableId, 423 int textColor, String title, String subTitle, int startX, int startY, int width, 424 int height) { 425 final int INSET = 0; 426 final int startHeight = startY + INSET; 427 final Drawable background = context.getResources().getDrawable(drawableId); 428 background.setBounds(startX + INSET, startHeight, startX + width - INSET, 429 height - INSET + startY); 430 background.draw(canvas); 431 432 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 433 p.setTypeface(Typeface.DEFAULT_BOLD); 434 p.setColor(textColor); 435 436 final int titleFontSize = height / 4; 437 final int maxWidth = width - (2 * INSET) - (2 * titleFontSize); 438 final int startYOffset = startHeight + (height / 2); 439 if (title != null) { 440 p.setTextSize(titleFontSize); 441 title = StringUtils.trimText(title, p, maxWidth); 442 canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2, 443 startYOffset - p.descent(), p); 444 } 445 446 if (subTitle != null) { 447 p.setTextSize(titleFontSize - 6); 448 subTitle = StringUtils.trimText(subTitle, p, maxWidth); 449 canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2, 450 startYOffset - p.ascent(), p); 451 } 452 } 453 } 454