blob: 58a5e76d63c0654bef1e166628b01a2358c0605c [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* 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 com.android.videoeditor.util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.Math;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.media.ExifInterface;
import android.util.Log;
import com.android.videoeditor.R;
import com.android.videoeditor.service.MovieOverlay;
/**
* Image utility methods
*/
public class ImageUtils {
/**
* Logging
*/
private static final String TAG = "ImageUtils";
// The resize paint
private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
// The match aspect ratio mode for scaleImage
public static int MATCH_SMALLER_DIMENSION = 1;
public static int MATCH_LARGER_DIMENSION = 2;
/**
* It is not possible to instantiate this class
*/
private ImageUtils() {
}
/**
* Resize a bitmap to the specified width and height.
*
* @param filename The filename
* @param width The thumbnail width
* @param height The thumbnail height
* @param match MATCH_SMALLER_DIMENSION or MATCH_LARGER_DIMMENSION
*
* @return The resized bitmap
*/
public static Bitmap scaleImage(String filename, int width, int height, int match)
throws IOException {
final BitmapFactory.Options dbo = new BitmapFactory.Options();
dbo.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filename, dbo);
final int nativeWidth = dbo.outWidth;
final int nativeHeight = dbo.outHeight;
final Bitmap srcBitmap;
float scaledWidth, scaledHeight;
final BitmapFactory.Options options = new BitmapFactory.Options();
if (nativeWidth > width || nativeHeight > height) {
float dx = ((float) nativeWidth) / ((float) width);
float dy = ((float) nativeHeight) / ((float) height);
float scale = (match == MATCH_SMALLER_DIMENSION) ? Math.max(dx,dy) : Math.min(dx,dy);
scaledWidth = nativeWidth / scale;
scaledHeight = nativeHeight / scale;
// Create the bitmap from file.
options.inSampleSize = (scale > 1.0f) ? ((int) scale) : 1;
} else {
scaledWidth = width;
scaledHeight = height;
options.inSampleSize = 1;
}
srcBitmap = BitmapFactory.decodeFile(filename, options);
if (srcBitmap == null) {
throw new IOException("Cannot decode file: " + filename);
}
// Create the canvas bitmap.
final Bitmap bitmap = Bitmap.createBitmap(Math.round(scaledWidth),
Math.round(scaledHeight),
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(srcBitmap,
new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight()),
new Rect(0, 0, Math.round(scaledWidth), Math.round(scaledHeight)),
sResizePaint);
// Release the source bitmap
srcBitmap.recycle();
return bitmap;
}
/**
* Rotate a JPEG according to the EXIF data
*
* @param inputFilename The name of the input file (must be a JPEG filename)
* @param outputFile The rotated file
*
* @return true if the image was rotated
*/
public static boolean transformJpeg(String inputFilename, File outputFile)
throws IOException {
final ExifInterface exif = new ExifInterface(inputFilename);
final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exif orientation: " + orientation);
}
// Degrees by which we rotate the image.
int degrees = 0;
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90: {
degrees = 90;
break;
}
case ExifInterface.ORIENTATION_ROTATE_180: {
degrees = 180;
break;
}
case ExifInterface.ORIENTATION_ROTATE_270: {
degrees = 270;
break;
}
}
rotateAndScaleImage(inputFilename, degrees, outputFile);
return degrees != 0;
}
/**
* Rotates an image according to the specified {@code orientation}.
* We limit the number of pixels of the scaled image. Thus the image
* will typically be downsampled.
*
* @param inputFilename The input filename
* @param orientation The rotation angle
* @param outputFile The output file
*/
private static void rotateAndScaleImage(String inputFilename, int orientation, File outputFile)
throws FileNotFoundException, IOException {
// In order to avoid OutOfMemoryError when rotating the image, we scale down the size of the
// input image. We set the maxmimum number of allowed pixels to 2M and scale down the image
// accordingly.
// Determine width and height of the original bitmap without allocating memory for it,
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile(inputFilename, opt);
// Determine the scale factor based on the ratio of pixel count over max allowed pixels.
final int width = opt.outWidth;
final int height = opt.outHeight;
final int pixelCount = width * height;
final int MAX_PIXELS_FOR_SCALED_IMAGE = 2000000;
double scale = Math.sqrt( (double) pixelCount / MAX_PIXELS_FOR_SCALED_IMAGE);
if (scale <= 1) {
scale = 1;
} else {
// Make the scale factor a power of 2 for faster processing. Also the resulting bitmap may
// have different dimensions than what has been requested if the scale factor is not a
// power of 2.
scale = nextPowerOf2((int) Math.ceil(scale));
}
// Load the scaled image.
BitmapFactory.Options opt2 = new BitmapFactory.Options();
opt2.inSampleSize = (int) scale;
final Bitmap scaledBmp = BitmapFactory.decodeFile(inputFilename, opt2);
// Rotation matrix used to rotate the image.
final Matrix mtx = new Matrix();
mtx.postRotate(orientation);
final Bitmap rotatedBmp = Bitmap.createBitmap(scaledBmp, 0, 0,
scaledBmp.getWidth(), scaledBmp.getHeight(), mtx, true);
scaledBmp.recycle();
// Save the rotated image to a file in the current project folder
final FileOutputStream fos = new FileOutputStream(outputFile);
rotatedBmp.compress(CompressFormat.JPEG, 100, fos);
fos.close();
rotatedBmp.recycle();
}
/**
* Returns the next power of two.
* Returns the input if it is already power of 2.
* Throws IllegalArgumentException if the input is <= 0 or the answer overflows.
*/
private static int nextPowerOf2(int n) {
if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException();
n -= 1;
n |= n >> 16;
n |= n >> 8;
n |= n >> 4;
n |= n >> 2;
n |= n >> 1;
return n + 1;
}
/**
* Build an overlay image
*
* @param context The context
* @param inputBitmap If the bitmap is provided no not create a new one
* @param overlayType The overlay type
* @param title The title
* @param subTitle The subtitle
* @param width The width
* @param height The height
*
* @return The bitmap
*/
public static Bitmap buildOverlayBitmap(Context context, Bitmap inputBitmap, int overlayType,
String title, String subTitle, int width, int height) {
final Bitmap overlayBitmap;
if (inputBitmap == null) {
overlayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} else {
overlayBitmap = inputBitmap;
}
overlayBitmap.eraseColor(Color.TRANSPARENT);
final Canvas canvas = new Canvas(overlayBitmap);
switch (overlayType) {
case MovieOverlay.OVERLAY_TYPE_CENTER_1: {
drawCenterOverlay(context, canvas, R.drawable.overlay_background_1,
Color.WHITE, title, subTitle, width, height);
break;
}
case MovieOverlay.OVERLAY_TYPE_BOTTOM_1: {
drawBottomOverlay(context, canvas, R.drawable.overlay_background_1,
Color.WHITE, title, subTitle, width, height);
break;
}
case MovieOverlay.OVERLAY_TYPE_CENTER_2: {
drawCenterOverlay(context, canvas, R.drawable.overlay_background_2,
Color.BLACK, title, subTitle, width, height);
break;
}
case MovieOverlay.OVERLAY_TYPE_BOTTOM_2: {
drawBottomOverlay(context, canvas, R.drawable.overlay_background_2,
Color.BLACK, title, subTitle, width, height);
break;
}
default: {
throw new IllegalArgumentException("Unsupported overlay type: " + overlayType);
}
}
return overlayBitmap;
}
/**
* Build an overlay image in the center third of the image
*
* @param context The context
* @param canvas The canvas
* @param drawableId The overlay background drawable if
* @param textColor The text color
* @param title The title
* @param subTitle The subtitle
* @param width The width
* @param height The height
*/
private static void drawCenterOverlay(Context context, Canvas canvas, int drawableId,
int textColor, String title, String subTitle, int width, int height) {
final int INSET = width / 72;
final int startHeight = (height / 3) + INSET;
final Drawable background = context.getResources().getDrawable(drawableId);
background.setBounds(INSET, startHeight, width - INSET,
((2 * height) / 3) - INSET);
background.draw(canvas);
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
p.setTypeface(Typeface.DEFAULT_BOLD);
p.setColor(textColor);
final int titleFontSize = height / 12;
final int maxWidth = width - (2 * INSET) - (2 * titleFontSize);
final int startYOffset = startHeight + (height / 6);
if (title != null) {
p.setTextSize(titleFontSize);
title = StringUtils.trimText(title, p, maxWidth);
canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2,
startYOffset - p.descent(), p);
}
if (subTitle != null) {
p.setTextSize(titleFontSize - 6);
subTitle = StringUtils.trimText(subTitle, p, maxWidth);
canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2,
startYOffset - p.ascent(), p);
}
}
/**
* Build an overlay image in the lower third of the image
*
* @param context The context
* @param canvas The canvas
* @param drawableId The overlay background drawable if
* @param textColor The text color
* @param title The title
* @param subTitle The subtitle
* @param width The width
* @param height The height
*/
private static void drawBottomOverlay(Context context, Canvas canvas, int drawableId,
int textColor, String title, String subTitle, int width, int height) {
final int INSET = width / 72;
final int startHeight = ((2 * height) / 3) + INSET;
final Drawable background = context.getResources().getDrawable(drawableId);
background.setBounds(INSET, startHeight, width - INSET, height - INSET);
background.draw(canvas);
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
p.setTypeface(Typeface.DEFAULT_BOLD);
p.setColor(textColor);
final int titleFontSize = height / 12;
final int maxWidth = width - (2 * INSET) - (2 * titleFontSize);
final int startYOffset = startHeight + (height / 6);
if (title != null) {
p.setTextSize(titleFontSize);
title = StringUtils.trimText(title, p, maxWidth);
canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2,
startYOffset - p.descent(), p);
}
if (subTitle != null) {
p.setTextSize(titleFontSize - 6);
subTitle = StringUtils.trimText(subTitle, p, maxWidth);
canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2,
startYOffset - p.ascent(), p);
}
}
/**
* Build an overlay preview image
*
* @param context The context
* @param canvas The canvas
* @param overlayType The overlay type
* @param title The title
* @param subTitle The subtitle
* @param startX The start horizontal position
* @param startY The start vertical position
* @param width The width
* @param height The height
*/
public static void buildOverlayPreview(Context context, Canvas canvas, int overlayType,
String title, String subTitle, int startX, int startY, int width, int height) {
switch (overlayType) {
case MovieOverlay.OVERLAY_TYPE_CENTER_1:
case MovieOverlay.OVERLAY_TYPE_BOTTOM_1: {
drawOverlayPreview(context, canvas, R.drawable.overlay_background_1,
Color.WHITE, title, subTitle, startX, startY, width, height);
break;
}
case MovieOverlay.OVERLAY_TYPE_CENTER_2:
case MovieOverlay.OVERLAY_TYPE_BOTTOM_2: {
drawOverlayPreview(context, canvas, R.drawable.overlay_background_2,
Color.BLACK, title, subTitle, startX, startY, width, height);
break;
}
default: {
throw new IllegalArgumentException("Unsupported overlay type: " + overlayType);
}
}
}
/**
* Build an overlay image in the lower third of the image
*
* @param context The context
* @param canvas The canvas
* @param drawableId The overlay background drawable if
* @param title The title
* @param subTitle The subtitle
* @param width The width
* @param height The height
*/
private static void drawOverlayPreview(Context context, Canvas canvas, int drawableId,
int textColor, String title, String subTitle, int startX, int startY, int width,
int height) {
final int INSET = 0;
final int startHeight = startY + INSET;
final Drawable background = context.getResources().getDrawable(drawableId);
background.setBounds(startX + INSET, startHeight, startX + width - INSET,
height - INSET + startY);
background.draw(canvas);
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
p.setTypeface(Typeface.DEFAULT_BOLD);
p.setColor(textColor);
final int titleFontSize = height / 4;
final int maxWidth = width - (2 * INSET) - (2 * titleFontSize);
final int startYOffset = startHeight + (height / 2);
if (title != null) {
p.setTextSize(titleFontSize);
title = StringUtils.trimText(title, p, maxWidth);
canvas.drawText(title, (width - (2 * INSET) - p.measureText(title)) / 2,
startYOffset - p.descent(), p);
}
if (subTitle != null) {
p.setTextSize(titleFontSize - 6);
subTitle = StringUtils.trimText(subTitle, p, maxWidth);
canvas.drawText(subTitle, (width - (2 * INSET) - p.measureText(subTitle)) / 2,
startYOffset - p.ascent(), p);
}
}
}