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 */ ImageUtils()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 */ scaleImage(String filename, int width, int height, int match)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 */ transformJpeg(String inputFilename, File outputFile)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 */ rotateAndScaleImage(String inputFilename, int orientation, File outputFile)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 */ nextPowerOf2(int n)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 */ buildOverlayBitmap(Context context, Bitmap inputBitmap, int overlayType, String title, String subTitle, int width, int height)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 */ drawCenterOverlay(Context context, Canvas canvas, int drawableId, int textColor, String title, String subTitle, int width, int height)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 */ drawBottomOverlay(Context context, Canvas canvas, int drawableId, int textColor, String title, String subTitle, int width, int height)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 */ buildOverlayPreview(Context context, Canvas canvas, int overlayType, String title, String subTitle, int startX, int startY, int width, int height)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 */ drawOverlayPreview(Context context, Canvas canvas, int drawableId, int textColor, String title, String subTitle, int startX, int startY, int width, int height)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