heifwriter: rotation support -- DO NOT MERGE

Add rotation support, and change public constructor to
Builder instead so that additional parameters can be
supported later.

Test: HeifWriterTest; local test app with rotation flag set.
Bug: 63633199
Change-Id: I04d8a3fd8ab454f85c200b0e2e4c8ab8f5917d5a
(cherry picked from commit 941fb082e942763b49abf2a1be83ae418b7253dc)
diff --git a/heifwriter/api/current.txt b/heifwriter/api/current.txt
index c7311ba..3c39776 100644
--- a/heifwriter/api/current.txt
+++ b/heifwriter/api/current.txt
@@ -1,8 +1,6 @@
 package androidx.heifwriter {
 
   public final class HeifWriter implements java.lang.AutoCloseable {
-    ctor public HeifWriter(java.lang.String, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
-    ctor public HeifWriter(java.io.FileDescriptor, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
     method public void addBitmap(android.graphics.Bitmap);
     method public void addYuvBuffer(int, byte[]);
     method public void close();
@@ -15,5 +13,17 @@
     field public static final int INPUT_MODE_SURFACE = 1; // 0x1
   }
 
+  public static final class HeifWriter.Builder {
+    ctor public HeifWriter.Builder(java.lang.String, int, int, int);
+    ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+    method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+    method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+    method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler);
+    method public androidx.heifwriter.HeifWriter.Builder setMaxImages(int);
+    method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(int);
+    method public androidx.heifwriter.HeifWriter.Builder setQuality(int);
+    method public androidx.heifwriter.HeifWriter.Builder setRotation(int);
+  }
+
 }
 
diff --git a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index d5b339a..2527002 100644
--- a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -224,7 +224,11 @@
     }
 
     private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
-        doTest(builder.setNumImages(4).build());
+        builder.setNumImages(4);
+        doTest(builder.setRotation(270).build());
+        doTest(builder.setRotation(180).build());
+        doTest(builder.setRotation(90).build());
+        doTest(builder.setRotation(0).build());
         doTest(builder.setNumImages(1).build());
         doTest(builder.setNumImages(8).build());
     }
@@ -252,102 +256,109 @@
     }
 
     private static class TestConfig {
-        final int inputMode;
-        final boolean useGrid;
-        final boolean useHandler;
-        final int maxNumImages;
-        final int numImages;
-        final int width;
-        final int height;
-        final int quality;
-        final String inputPath;
-        final String outputPath;
-        final Bitmap[] bitmaps;
+        final int mInputMode;
+        final boolean mUseGrid;
+        final boolean mUseHandler;
+        final int mMaxNumImages;
+        final int mNumImages;
+        final int mWidth;
+        final int mHeight;
+        final int mRotation;
+        final int mQuality;
+        final String mInputPath;
+        final String mOutputPath;
+        final Bitmap[] mBitmaps;
 
-        TestConfig(int _inputMode, boolean _useGrid, boolean _useHandler,
-                   int _maxNumImage, int _numImages, int _width, int _height, int _quality,
-                   String _inputPath, String _outputPath, Bitmap[] _bitmaps) {
-            inputMode = _inputMode;
-            useGrid = _useGrid;
-            useHandler = _useHandler;
-            maxNumImages = _maxNumImage;
-            numImages = _numImages;
-            width = _width;
-            height = _height;
-            quality = _quality;
-            inputPath = _inputPath;
-            outputPath = _outputPath;
-            bitmaps = _bitmaps;
+        TestConfig(int inputMode, boolean useGrid, boolean useHandler,
+                   int maxNumImages, int numImages, int width, int height,
+                   int rotation, int quality,
+                   String inputPath, String outputPath, Bitmap[] bitmaps) {
+            mInputMode = inputMode;
+            mUseGrid = useGrid;
+            mUseHandler = useHandler;
+            mMaxNumImages = maxNumImages;
+            mNumImages = numImages;
+            mWidth = width;
+            mHeight = height;
+            mRotation = rotation;
+            mQuality = quality;
+            mInputPath = inputPath;
+            mOutputPath = outputPath;
+            mBitmaps = bitmaps;
         }
 
         static class Builder {
-            final int inputMode;
-            final boolean useGrid;
-            final boolean useHandler;
-            int maxNumImages;
-            int numImages;
-            int width;
-            int height;
-            int quality;
-            String inputPath;
-            final String outputPath;
-            Bitmap[] bitmaps;
-
-            boolean numImagesSetExplicitly;
+            final int mInputMode;
+            final boolean mUseGrid;
+            final boolean mUseHandler;
+            int mMaxNumImages;
+            int mNumImages;
+            int mWidth;
+            int mHeight;
+            int mRotation;
+            final int mQuality;
+            String mInputPath;
+            final String mOutputPath;
+            Bitmap[] mBitmaps;
+            boolean mNumImagesSetExplicitly;
 
 
-            Builder(int _inputMode, boolean _useGrids, boolean _useHandler) {
-                inputMode = _inputMode;
-                useGrid = _useGrids;
-                useHandler = _useHandler;
-                maxNumImages = numImages = 4;
-                width = 1920;
-                height = 1080;
-                quality = 100;
-                outputPath = new File(Environment.getExternalStorageDirectory(),
+            Builder(int inputMode, boolean useGrids, boolean useHandler) {
+                mInputMode = inputMode;
+                mUseGrid = useGrids;
+                mUseHandler = useHandler;
+                mMaxNumImages = mNumImages = 4;
+                mWidth = 1920;
+                mHeight = 1080;
+                mRotation = 0;
+                mQuality = 100;
+                mOutputPath = new File(Environment.getExternalStorageDirectory(),
                         OUTPUT_FILENAME).getAbsolutePath();
             }
 
-            Builder setInputPath(String _inputPath) {
-                inputPath = (inputMode == INPUT_MODE_BITMAP) ? _inputPath : null;
+            Builder setInputPath(String inputPath) {
+                mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
                 return this;
             }
 
-            Builder setNumImages(int _numImages) {
-                numImagesSetExplicitly = true;
-                numImages = _numImages;
+            Builder setNumImages(int numImages) {
+                mNumImagesSetExplicitly = true;
+                mNumImages = numImages;
+                return this;
+            }
+
+            Builder setRotation(int rotation) {
+                mRotation = rotation;
                 return this;
             }
 
             private void loadBitmapInputs() {
-                if (inputMode != INPUT_MODE_BITMAP) {
+                if (mInputMode != INPUT_MODE_BITMAP) {
                     return;
                 }
                 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-                retriever.setDataSource(inputPath);
+                retriever.setDataSource(mInputPath);
                 String hasImage = retriever.extractMetadata(
                         MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
                 if (!"yes".equals(hasImage)) {
                     throw new IllegalArgumentException("no bitmap found!");
                 }
-                width = Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH));
-                height = Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT));
-                maxNumImages = Math.min(maxNumImages, Integer.parseInt(retriever.extractMetadata(
+                mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
                         MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
-                if (!numImagesSetExplicitly) {
-                    numImages = maxNumImages;
+                if (!mNumImagesSetExplicitly) {
+                    mNumImages = mMaxNumImages;
                 }
-                bitmaps = new Bitmap[maxNumImages];
-                for (int i = 0; i < bitmaps.length; i++) {
-                    bitmaps[i] = retriever.getImageAtIndex(i);
+                mBitmaps = new Bitmap[mMaxNumImages];
+                for (int i = 0; i < mBitmaps.length; i++) {
+                    mBitmaps[i] = retriever.getImageAtIndex(i);
                 }
+                mWidth = mBitmaps[0].getWidth();
+                mHeight = mBitmaps[0].getHeight();
                 retriever.release();
             }
 
             private void cleanupStaleOutputs() {
-                File outputFile = new File(outputPath);
+                File outputFile = new File(mOutputPath);
                 if (outputFile.exists()) {
                     outputFile.delete();
                 }
@@ -357,58 +368,61 @@
                 cleanupStaleOutputs();
                 loadBitmapInputs();
 
-                return new TestConfig(inputMode, useGrid, useHandler, maxNumImages, numImages,
-                        width, height, quality, inputPath, outputPath, bitmaps);
+                return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
+                        mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
             }
         }
 
         @Override
         public String toString() {
-            return "TestConfig" +
-                    ": inputMode " + inputMode +
-                    ", useGrid " + useGrid +
-                    ", useHandler " + useHandler +
-                    ", maxNumImages " + maxNumImages +
-                    ", numImages " + numImages +
-                    ", width " + width +
-                    ", height " + height +
-                    ", quality " + quality +
-                    ", inputPath " + inputPath +
-                    ", outputPath " + outputPath;
+            return "TestConfig"
+                    + ": mInputMode " + mInputMode
+                    + ", mUseGrid " + mUseGrid
+                    + ", mUseHandler " + mUseHandler
+                    + ", mMaxNumImages " + mMaxNumImages
+                    + ", mNumImages " + mNumImages
+                    + ", mWidth " + mWidth
+                    + ", mHeight " + mHeight
+                    + ", mRotation " + mRotation
+                    + ", mQuality " + mQuality
+                    + ", mInputPath " + mInputPath
+                    + ", mOutputPath " + mOutputPath;
         }
     }
 
-    private void doTest(TestConfig testConfig) throws Exception {
-        int width = testConfig.width;
-        int height = testConfig.height;
-        int numImages = testConfig.numImages;
+    private void doTest(TestConfig config) throws Exception {
+        int width = config.mWidth;
+        int height = config.mHeight;
+        int numImages = config.mNumImages;
 
         mInputIndex = 0;
         HeifWriter heifWriter = null;
         FileInputStream inputStream = null;
         FileOutputStream outputStream = null;
         try {
-            if (DEBUG) Log.d(TAG, "started: " + testConfig);
+            if (DEBUG) Log.d(TAG, "started: " + config);
 
-            heifWriter = new HeifWriter(testConfig.outputPath, width, height,
-                    testConfig.useGrid,
-                    testConfig.quality,
-                    testConfig.maxNumImages,
-                    testConfig.maxNumImages - 1,
-                    testConfig.inputMode,
-                    testConfig.useHandler ? mHandler : null);
+            heifWriter = new HeifWriter.Builder(
+                    config.mOutputPath, width, height, config.mInputMode)
+                    .setRotation(config.mRotation)
+                    .setGridEnabled(config.mUseGrid)
+                    .setMaxImages(config.mMaxNumImages)
+                    .setQuality(config.mQuality)
+                    .setPrimaryIndex(config.mMaxNumImages - 1)
+                    .setHandler(config.mUseHandler ? mHandler : null)
+                    .build();
 
-            if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+            if (config.mInputMode == INPUT_MODE_SURFACE) {
                 mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
             }
 
             heifWriter.start();
 
-            if (testConfig.inputMode == INPUT_MODE_BUFFER) {
+            if (config.mInputMode == INPUT_MODE_BUFFER) {
                 byte[] data = new byte[width * height * 3 / 2];
 
-                if (testConfig.inputPath != null) {
-                    inputStream = new FileInputStream(testConfig.inputPath);
+                if (config.mInputPath != null) {
+                    inputStream = new FileInputStream(config.mInputPath);
                 }
 
                 if (DUMP_YUV_INPUT) {
@@ -426,7 +440,7 @@
                     }
                     heifWriter.addYuvBuffer(ImageFormat.YUV_420_888, data);
                 }
-            } else if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+            } else if (config.mInputMode == INPUT_MODE_SURFACE) {
                 // The input surface is a surface texture using single buffer mode, draws will be
                 // blocked until onFrameAvailable is done with the buffer, which is dependant on
                 // how fast MediaCodec processes them, which is further dependent on how fast the
@@ -438,8 +452,8 @@
                 }
                 heifWriter.setInputEndOfStreamTimestamp(
                         1000 * computePresentationTime(numImages - 1));
-            } else if (testConfig.inputMode == INPUT_MODE_BITMAP) {
-                Bitmap[] bitmaps = testConfig.bitmaps;
+            } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+                Bitmap[] bitmaps = config.mBitmaps;
                 for (int i = 0; i < Math.min(bitmaps.length, numImages); i++) {
                     if (DEBUG) Log.d(TAG, "addBitmap: " + i);
                     heifWriter.addBitmap(bitmaps[i]);
@@ -448,8 +462,8 @@
             }
 
             heifWriter.stop(3000);
-            verifyResult(testConfig.outputPath, width, height, testConfig.useGrid,
-                    Math.min(numImages, testConfig.maxNumImages));
+            verifyResult(config.mOutputPath, width, height, config.mRotation, config.mUseGrid,
+                    Math.min(numImages, config.mMaxNumImages));
             if (DEBUG) Log.d(TAG, "finished: PASS");
         } finally {
             try {
@@ -532,7 +546,7 @@
     }
 
     private void verifyResult(
-            String filename, int width, int height, boolean useGrid, int numImages)
+            String filename, int width, int height, int rotation, boolean useGrid, int numImages)
             throws Exception {
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
         retriever.setDataSource(filename);
@@ -542,13 +556,16 @@
         }
         assertEquals("Wrong image count", numImages,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
         assertEquals("Wrong width", width,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
         assertEquals("Wrong height", height,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+        assertEquals("Wrong rotation", rotation,
+                Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
         retriever.release();
 
         if (useGrid) {
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 3020b96..40b1835 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -213,7 +213,14 @@
 
         int gridWidth, gridHeight, gridRows, gridCols;
 
-        useGrid = useGrid && (width > GRID_WIDTH || height > GRID_HEIGHT);
+        MediaCodecInfo.CodecCapabilities caps =
+                mEncoder.getCodecInfo().getCapabilitiesForType(useHeicEncoder
+                        ? MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC
+                        : MediaFormat.MIMETYPE_VIDEO_HEVC);
+
+        useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+        // Always enable grid if the size is too large for the HEVC encoder
+        useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
 
         if (useGrid) {
             gridWidth = GRID_WIDTH;
@@ -258,15 +265,11 @@
 
         codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
         codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-
-        MediaCodecInfo.CodecCapabilities caps =
-                mEncoder.getCodecInfo().getCapabilitiesForType(useHeicEncoder
-                        ? MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC
-                        : MediaFormat.MIMETYPE_VIDEO_HEVC);
-        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
         codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
         codecFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, mNumTiles * 30);
+
+        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
         if (encoderCaps.isBitrateModeSupported(
                 MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
             Log.d(TAG, "Setting bitrate mode to constant quality");
@@ -278,14 +281,14 @@
                             (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
         } else {
             if (encoderCaps.isBitrateModeSupported(
-                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)) {
-                Log.d(TAG, "Setting bitrate mode to variable bitrate");
-                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
-                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
-            } else { // assume CBR
+                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
                 Log.d(TAG, "Setting bitrate mode to constant bitrate");
                 codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
                         MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+            } else { // assume VBR
+                Log.d(TAG, "Setting bitrate mode to variable bitrate");
+                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
             }
             // Calculate the bitrate based on image dimension, max compression ratio and quality.
             // Note that we set the frame rate to the number of tiles, so the bitrate would be the
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index be7dffb..fff1a9b 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -16,6 +16,8 @@
 
 package androidx.heifwriter;
 
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
 import android.annotation.SuppressLint;
 import android.graphics.Bitmap;
 import android.media.MediaCodec;
@@ -25,11 +27,12 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Process;
+import android.util.Log;
+import android.view.Surface;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import android.util.Log;
-import android.view.Surface;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -38,8 +41,6 @@
 import java.nio.ByteBuffer;
 import java.util.concurrent.TimeoutException;
 
-import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
-
 /**
  * This class writes one or more still images (of the same dimensions) into
  * a heif file.
@@ -79,7 +80,8 @@
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private int mNumTiles;
-    private final int mNumImages;
+    private final int mRotation;
+    private final int mMaxImages;
     private final int mPrimaryIndex;
     private final ResultWaiter mResultWaiter = new ResultWaiter();
 
@@ -119,96 +121,200 @@
     public @interface InputMode {}
 
     /**
-     * Construct a heif writer that writes to a file specified by its path.
-     *
-     * @param path Path of the file to be written.
-     * @param width Width of the image.
-     * @param height Height of the image.
-     * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
-     *                automatically chosen.
-     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
-     *                supported by this implementation (which often results in larger file size).
-     * @param numImages Max number of images to write. Frames exceeding this number will not be
-     *                  written to file. The writing can be stopped earlier before this number of
-     *                  images are written by {@link #stop(long)}, except for the input mode of
-     *                  {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
-     *                 {@link #setInputEndOfStreamTimestamp(long)} and reached.
-     * @param primaryIndex Index of the image that should be marked as primary, must be within range
-     *                     [0, numImages - 1] inclusive.
-     * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
-     *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
-     * @param handler If not null, client will receive all callbacks on the handler's looper.
-     *                Otherwise, client will receive callbacks on a looper created by the writer.
-     *
-     * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
+     * Builder class for constructing a HeifWriter object from specified parameters.
      */
-    @SuppressLint("WrongConstant")
-    public HeifWriter(@NonNull String path,
-                      int width, int height, boolean useGrid,
-                      int quality, int numImages, int primaryIndex,
-                      @InputMode int inputMode,
-                      @Nullable Handler handler) throws IOException {
-        this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
-                new MediaMuxer(path, MUXER_OUTPUT_HEIF));
+    public static final class Builder {
+        private final String mPath;
+        private final FileDescriptor mFd;
+        private final int mWidth;
+        private final int mHeight;
+        private final @InputMode int mInputMode;
+        private boolean mGridEnabled = true;
+        private int mQuality = 100;
+        private int mMaxImages = 1;
+        private int mPrimaryIndex = 0;
+        private int mRotation = 0;
+        private Handler mHandler;
+
+        /**
+         * Construct a Builder with output specified by its path.
+         *
+         * @param path Path of the file to be written.
+         * @param width Width of the image.
+         * @param height Height of the image.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull String path,
+                       int width, int height, @InputMode int inputMode) {
+            this(path, null, width, height, inputMode);
+        }
+
+        /**
+         * Construct a Builder with output specified by its file descriptor.
+         *
+         * @param fd File descriptor of the file to be written.
+         * @param width Width of the image.
+         * @param height Height of the image.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull FileDescriptor fd,
+                       int width, int height, @InputMode int inputMode) {
+            this(null, fd, width, height, inputMode);
+        }
+
+        private Builder(String path, FileDescriptor fd,
+                        int width, int height, @InputMode int inputMode) {
+            if (width <= 0 || height <= 0) {
+                throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
+            }
+            mPath = path;
+            mFd = fd;
+            mWidth = width;
+            mHeight = height;
+            mInputMode = inputMode;
+        }
+
+        /**
+         * Set the image rotation in degrees.
+         *
+         * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
+         *                 Default is 0.
+         * @return this Builder object.
+         */
+        public Builder setRotation(int rotation) {
+            if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+                throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+            }
+            mRotation = rotation;
+            return this;
+        }
+
+        /**
+         * Set whether to enable grid option.
+         *
+         * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+         *                    automatically chosen. Default is to enable.
+         * @return this Builder object.
+         */
+        public Builder setGridEnabled(boolean gridEnabled) {
+            mGridEnabled = gridEnabled;
+            return this;
+        }
+
+        /**
+         * Set the quality for encoding images.
+         *
+         * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+         *                quality supported by this implementation. Default is 100.
+         * @return this Builder object.
+         */
+        public Builder setQuality(int quality) {
+            if (quality < 0 || quality > 100) {
+                throw new IllegalArgumentException("Invalid quality: " + quality);
+            }
+            mQuality = quality;
+            return this;
+        }
+
+        /**
+         * Set the maximum number of images to write.
+         *
+         * @param maxImages Max number of images to write. Frames exceeding this number will not be
+         *                  written to file. The writing can be stopped earlier before this number
+         *                  of images are written by {@link #stop(long)}, except for the input mode
+         *                  of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+         *                  specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+         *                  Default is 1.
+         * @return this Builder object.
+         */
+        public Builder setMaxImages(int maxImages) {
+            if (maxImages <= 0) {
+                throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+            }
+            mMaxImages = maxImages;
+            return this;
+        }
+
+        /**
+         * Set the primary image index.
+         *
+         * @param primaryIndex Index of the image that should be marked as primary, must be within
+         *                     range [0, maxImages - 1] inclusive. Default is 0.
+         * @return this Builder object.
+         */
+        public Builder setPrimaryIndex(int primaryIndex) {
+            if (primaryIndex < 0) {
+                throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
+            }
+            mPrimaryIndex = primaryIndex;
+            return this;
+        }
+
+        /**
+         * Provide a handler for the HeifWriter to use.
+         *
+         * @param handler If not null, client will receive all callbacks on the handler's looper.
+         *                Otherwise, client will receive callbacks on a looper created by the
+         *                writer. Default is null.
+         * @return this Builder object.
+         */
+        public Builder setHandler(@Nullable Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
+        /**
+         * Build a HeifWriter object.
+         *
+         * @return a HeifWriter object built according to the specifications.
+         * @throws IOException if failed to create the writer, possibly due to failure to create
+         *                     {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+         */
+        public HeifWriter build() throws IOException {
+            return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+                    mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+        }
     }
 
-    /**
-     * Construct a heif writer that writes to a file specified by file descriptor.
-     *
-     * @param fd File descriptor of the file to be written.
-     * @param width Width of the image.
-     * @param height Height of the image.
-     * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
-     *                automatically chosen.
-     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
-     *                supported by this implementation (which often results in larger file size).
-     * @param numImages Max number of images to write. Frames exceeding this number will not be
-     *                  written to file. The writing can be stopped earlier before this number of
-     *                  images are written by {@link #stop(long)}, except for the input mode of
-     *                  {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
-     *                 {@link #setInputEndOfStreamTimestamp(long)} and reached.
-     * @param primaryIndex Index of the image that should be marked as primary, must be within range
-     *                     [0, numImages - 1] inclusive.
-     * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
-     *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
-     * @param handler If not null, client will receive all callbacks on the handler's looper.
-     *                Otherwise, client will receive callbacks on a looper created by the writer.
-     *
-     * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
-     */
     @SuppressLint("WrongConstant")
-    public HeifWriter(@NonNull FileDescriptor fd,
-                      int width, int height, boolean useGrid,
-                      int quality, int numImages, int primaryIndex,
-                      @InputMode int inputMode,
-                      @Nullable Handler handler) throws IOException {
-        this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
-                new MediaMuxer(fd, MUXER_OUTPUT_HEIF));
-    }
-
-    private HeifWriter(int width, int height, boolean useGrid,
-                       int quality, int numImages, int primaryIndex,
+    private HeifWriter(@NonNull String path,
+                       @NonNull FileDescriptor fd,
+                       int width,
+                       int height,
+                       int rotation,
+                       boolean gridEnabled,
+                       int quality,
+                       int maxImages,
+                       int primaryIndex,
                        @InputMode int inputMode,
-                       @Nullable Handler handler,
-                       @NonNull MediaMuxer muxer) throws IOException {
-        if (numImages <= 0 || primaryIndex < 0 || primaryIndex >= numImages) {
+                       @Nullable Handler handler) throws IOException {
+        if (primaryIndex >= maxImages) {
             throw new IllegalArgumentException(
-                    "Invalid numImages (" + numImages + ") or primaryIndex (" + primaryIndex + ")");
+                    "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "width: " + width
+                    + ", height: " + height
+                    + ", rotation: " + rotation
+                    + ", gridEnabled: " + gridEnabled
+                    + ", quality: " + quality
+                    + ", maxImages: " + maxImages
+                    + ", primaryIndex: " + primaryIndex
+                    + ", inputMode: " + inputMode);
         }
 
         MediaFormat format = MediaFormat.createVideoFormat(
                 MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, width, height);
 
-        if (DEBUG) {
-            Log.d(TAG, "format: " + format + ", inputMode: " + inputMode +
-                    ", numImage: " + numImages + ", primaryIndex: " + primaryIndex);
-        }
-
         // set to 1 initially, and wait for output format to know for sure
         mNumTiles = 1;
 
+        mRotation = rotation;
         mInputMode = inputMode;
-        mNumImages = numImages;
+        mMaxImages = maxImages;
         mPrimaryIndex = primaryIndex;
 
         Looper looper = (handler != null) ? handler.getLooper() : null;
@@ -222,9 +328,10 @@
         }
         mHandler = new Handler(looper);
 
-        mMuxer = muxer;
+        mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+                                : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
 
-        mHeifEncoder = new HeifEncoder(width, height, useGrid, quality,
+        mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
                 mInputMode, mHandler, new HeifCallback());
     }
 
@@ -406,13 +513,17 @@
                 mNumTiles = 1;
             }
 
-            // add mNumImages image tracks of the same format
-            mTrackIndexArray = new int[mNumImages];
+            // add mMaxImages image tracks of the same format
+            mTrackIndexArray = new int[mMaxImages];
+
+            // set rotation angle
+            if (mRotation > 0) {
+                Log.d(TAG, "setting rotation: " + mRotation);
+                mMuxer.setOrientationHint(mRotation);
+            }
             for (int i = 0; i < mTrackIndexArray.length; i++) {
                 // mark primary
-                if (i == mPrimaryIndex) {
-                    format.setInteger(MediaFormat.KEY_IS_DEFAULT, 1);
-                }
+                format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
                 mTrackIndexArray[i] = mMuxer.addTrack(format);
             }
             mMuxer.start();
@@ -436,7 +547,7 @@
                 return;
             }
 
-            if (mOutputIndex < mNumImages * mNumTiles) {
+            if (mOutputIndex < mMaxImages * mNumTiles) {
                 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                 info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
                 mMuxer.writeSampleData(
@@ -446,7 +557,7 @@
             mOutputIndex++;
 
             // post EOS if reached max number of images allowed.
-            if (mOutputIndex == mNumImages * mNumTiles) {
+            if (mOutputIndex == mMaxImages * mNumTiles) {
                 stopAndNotify(null);
             }
         }