chromium/components/browser_ui/photo_picker/android/java/src/org/chromium/components/browser_ui/photo_picker/BitmapUtils.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.photo_picker;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.media.MediaMetadataRetriever;
import android.util.Pair;

import org.chromium.base.metrics.RecordHistogram;

import java.io.FileDescriptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/** A collection of utility functions for dealing with bitmaps. */
class BitmapUtils {
    // Constants used to log UMA enum histogram, must stay in sync with the
    // ExifOrientation enum in enums.xml. Further actions can only be appended,
    // existing entries must not be overwritten.
    private static final int EXIF_ORIENTATION_NORMAL = 0;
    private static final int EXIF_ORIENTATION_ROTATE_90 = 1;
    private static final int EXIF_ORIENTATION_ROTATE_180 = 2;
    private static final int EXIF_ORIENTATION_ROTATE_270 = 3;
    private static final int EXIF_ORIENTATION_TRANSPOSE = 4;
    private static final int EXIF_ORIENTATION_TRANSVERSE = 5;
    private static final int EXIF_ORIENTATION_FLIP_HORIZONTAL = 6;
    private static final int EXIF_ORIENTATION_FLIP_VERTICAL = 7;
    private static final int EXIF_ORIENTATION_UNDEFINED = 8;
    private static final int EXIF_ORIENTATION_ACTION_BOUNDARY = 9;

    /**
     * Takes a |bitmap| and (if |!fullWidth|) returns a square thumbnail of |width|x|width| from the
     * center of the bitmap specified, or (if |fullWidth|) an image that scaled to fit within
     * |width|. The image is rotated according to the Exif information, if needed (on Nougat and up
     * only).
     *
     * @param bitmap The bitmap to adjust.
     * @param width The desired width (and height if fullWidth is false).
     * @param fullWidth Whether full screen width is in use. When true, the image returned is
     *     |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
     *     image is returned.
     * @param descriptor The file descriptor to read the Exif information from.
     * @return The new bitmap thumbnail.
     */
    private static Bitmap sizeBitmap(
            Bitmap bitmap, int width, boolean fullWidth, FileDescriptor descriptor) {
        // TODO(finnur): Investigate options that require fewer bitmaps to be created.
        if (!fullWidth) {
            bitmap = ensureMinSize(bitmap, width);
            bitmap = rotateAndCropToSquare(bitmap, width, descriptor);
            return bitmap;
        } else {
            return rotateAndFitToMaxWidth(bitmap, width, descriptor);
        }
    }

    /**
     * Given a FileDescriptor, decodes the contents and returns a square thumbnail of
     * |width|x|width| from the center of the bitmap specified, or (if |fullwidth|) an image that
     * scaled to fit within |width|. The image is rotated according to the Exif information, if
     * needed (on Nougat and up only).
     *
     * @param descriptor The FileDescriptor for the file to read.
     * @param size The width of the bitmap to return.
     * @param fullWidth Whether full screen width is in use. When true, the image returned is
     *     |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
     *     image is returned.
     * @return The resulting bitmap and its ratio.
     */
    public static Pair<Bitmap, Float> decodeBitmapFromFileDescriptor(
            FileDescriptor descriptor, int size, boolean fullWidth) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(descriptor, null, options);
        options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, size);
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFileDescriptor(descriptor, null, options);

        if (bitmap == null) return null;

        return new Pair<Bitmap, Float>(
                sizeBitmap(bitmap, size, fullWidth, descriptor),
                (float) bitmap.getHeight() / bitmap.getWidth());
    }

    /**
     * Given a FileDescriptor, decodes the video and returns a square thumbnail of |width|x|width|
     * from the center of the bitmap specified, or (if |fullwidth|) an image that scaled to fit
     * within |width|. The image is rotated according to the Exif information, if needed (on Nougat
     * and up only).
     *
     * @param retriever The MediaMetadataRetriever to use (must have source already set).
     * @param descriptor The FileDescriptor for the file to read.
     * @param width The width of the bitmap to return.
     * @param frames The number of frames to extract.
     * @param fullWidth Whether full screen width is in use. When true, the image returned is
     *     |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
     *     image is returned.
     * @param intervalMs The interval between frames (in milliseconds).
     * @return A list of extracted frames.
     */
    public static Pair<List<Bitmap>, Float> decodeVideoFromFileDescriptor(
            MediaMetadataRetriever retriever,
            FileDescriptor descriptor,
            int width,
            int frames,
            boolean fullWidth,
            long intervalMs) {
        List<Bitmap> bitmaps = new ArrayList<Bitmap>();
        Bitmap bitmap = null;
        Float ratio = null;
        for (int frame = 0; frame < frames; ++frame) {
            bitmap = retriever.getFrameAtTime(frame * intervalMs * 1000);
            if (bitmap == null) continue;
            if (ratio == null) ratio = (float) bitmap.getHeight() / bitmap.getWidth();

            bitmap = sizeBitmap(bitmap, width, fullWidth, descriptor);
            bitmaps.add(bitmap);
        }

        return new Pair<List<Bitmap>, Float>(bitmaps, ratio);
    }

    /**
     * Calculates the sub-sampling factor {@link BitmapFactory#inSampleSize} option for a given
     * image dimensions, which will be used to create a bitmap of a pre-determined size (as small as
     * possible without either dimension shrinking below |minSize|.
     *
     * @param width The calculated width of the image to decode.
     * @param height The calculated height of the image to decode.
     * @param minSize The maximum size the image should be (in either dimension).
     * @return The sub-sampling factor (power of two: 1 = no change, 2 = half-size, etc).
     */
    private static int calculateInSampleSize(int width, int height, int minSize) {
        int inSampleSize = 1;
        if (width > minSize && height > minSize) {
            inSampleSize = Math.min(width, height) / minSize;
        }
        return inSampleSize;
    }

    /**
     * Ensures a |bitmap| is at least |size| in both width and height.
     *
     * @param bitmap The bitmap to modify.
     * @param size The minimum size (width and height).
     * @return The resulting (scaled) bitmap.
     */
    private static Bitmap ensureMinSize(Bitmap bitmap, int size) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        if (width == size && height == size) return bitmap;

        if (width > size && height > size) {
            // Both sides are larger than requested, which will lead to excessive amount of
            // cropping. Shrink to a more manageable amount (shorter side becomes |size| in length).
            float scale = (width < height) ? (float) width / size : (float) height / size;
            width = Math.round(width / scale);
            height = Math.round(height / scale);
            return Bitmap.createScaledBitmap(bitmap, width, height, true);
        }

        if (width < size) {
            float scale = (float) size / width;
            width = size;
            height = (int) (height * scale);
        }

        if (height < size) {
            float scale = (float) size / height;
            height = size;
            width = (int) (width * scale);
        }

        return Bitmap.createScaledBitmap(bitmap, width, height, true);
    }

    /**
     * Records the Exif histogram value for a photo.
     *
     * @param sample The sample to record.
     */
    private static void recordExifHistogram(int sample) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.PhotoPicker.ExifOrientation", sample, EXIF_ORIENTATION_ACTION_BOUNDARY);
    }

    /**
     * Returns the rotation matrix from the Exif information in the file descriptor (on Nougat and
     * up only).
     *
     * @param descriptor The FileDescriptor containing the Exif information.
     * @return The resulting rotation matrix.
     */
    private static Matrix getRotationMatrix(FileDescriptor descriptor) {
        Matrix matrix = new Matrix();
        try {
            ExifInterface exif = new ExifInterface(descriptor);
            int rotation =
                    exif.getAttributeInt(
                            ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
            switch (rotation) {
                case ExifInterface.ORIENTATION_NORMAL:
                    recordExifHistogram(EXIF_ORIENTATION_NORMAL);
                    break;
                case ExifInterface.ORIENTATION_ROTATE_90:
                    matrix.postRotate(90);
                    recordExifHistogram(EXIF_ORIENTATION_ROTATE_90);
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    matrix.postRotate(180);
                    recordExifHistogram(EXIF_ORIENTATION_ROTATE_180);
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    matrix.postRotate(-90);
                    recordExifHistogram(EXIF_ORIENTATION_ROTATE_270);
                    break;
                case ExifInterface.ORIENTATION_TRANSPOSE:
                    matrix.setRotate(90);
                    matrix.postScale(-1, 1);
                    recordExifHistogram(EXIF_ORIENTATION_TRANSPOSE);
                    break;
                case ExifInterface.ORIENTATION_TRANSVERSE:
                    matrix.setRotate(-90);
                    matrix.postScale(-1, 1);
                    recordExifHistogram(EXIF_ORIENTATION_TRANSVERSE);
                    break;
                case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                    matrix.setScale(-1, 1);
                    recordExifHistogram(EXIF_ORIENTATION_FLIP_HORIZONTAL);
                    break;
                case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                    matrix.setScale(1, -1);
                    recordExifHistogram(EXIF_ORIENTATION_FLIP_VERTICAL);
                    break;
                case ExifInterface.ORIENTATION_UNDEFINED:
                    recordExifHistogram(EXIF_ORIENTATION_UNDEFINED);
                    break;
                default:
                    break;
            }
        } catch (IOException e) {
        }
        return matrix;
    }

    /**
     * Crops a |bitmap| to a certain square |size| and (on Nougat and up only) rotates it according
     * to the Exif information, if needed.
     *
     * @param bitmap The bitmap to crop.
     * @param size The size desired (width and height).
     * @param descriptor The FileDescriptor containing the Exif information.
     * @return The resulting (square) bitmap.
     */
    private static Bitmap rotateAndCropToSquare(
            Bitmap bitmap, int size, FileDescriptor descriptor) {
        int x = 0;
        int y = 0;
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        if (width == size && height == size) return bitmap;

        if (width > size) x = (width - size) / 2;
        if (height > size) y = (height - size) / 2;
        return Bitmap.createBitmap(bitmap, x, y, size, size, getRotationMatrix(descriptor), true);
    }

    /**
     * Rotate a bitmap according to its Exif information and make sure it fits to the maximum width.
     *
     * @param bitmap The input bitmap.
     * @param maxWidth The maximum width available.
     * @param descriptor The FileDescriptor containing the Exif information.
     */
    private static Bitmap rotateAndFitToMaxWidth(
            Bitmap bitmap, int maxWidth, FileDescriptor descriptor) {
        Bitmap rotated =
                Bitmap.createBitmap(
                        bitmap,
                        0,
                        0,
                        bitmap.getWidth(),
                        bitmap.getHeight(),
                        getRotationMatrix(descriptor),
                        true);
        float ratio = (float) maxWidth / rotated.getWidth();
        int height = (int) (rotated.getHeight() * ratio);
        return Bitmap.createScaledBitmap(rotated, maxWidth, height, true);
    }

    /**
     * Scales a |bitmap| to a certain size.
     *
     * @param bitmap The bitmap to scale.
     * @param scaleMaxSize What to scale it to.
     * @param filter True if the source should be filtered.
     * @return The resulting scaled bitmap.
     */
    public static Bitmap scale(Bitmap bitmap, float scaleMaxSize, boolean filter) {
        float ratio =
                Math.min(
                        (float) scaleMaxSize / bitmap.getWidth(),
                        (float) scaleMaxSize / bitmap.getHeight());
        int height = Math.round(ratio * bitmap.getHeight());
        int width = Math.round(ratio * bitmap.getWidth());

        return Bitmap.createScaledBitmap(bitmap, width, height, filter);
    }
}