1 /*
2  * Copyright 2022 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 androidx.camera.integration.extensions.utils
18 
19 import android.graphics.Matrix
20 import android.graphics.Rect
21 import android.graphics.RectF
22 import android.util.Size
23 import android.view.Surface
24 import android.view.TextureView
25 import androidx.core.graphics.toRect
26 
27 // Normalized space (-1, -1) - (1, 1).
28 private val NORMALIZED_RECT = RectF(-1f, -1f, 1f, 1f)
29 
30 object TransformUtil {
31 
32     /** Converts [Surface] rotation to rotation degrees: 90, 180, 270 or 0. */
33     @JvmStatic
surfaceRotationToRotationDegreesnull34     fun surfaceRotationToRotationDegrees(rotationValue: Int): Int =
35         when (rotationValue) {
36             Surface.ROTATION_0 -> 0
37             Surface.ROTATION_90 -> 90
38             Surface.ROTATION_180 -> 180
39             Surface.ROTATION_270 -> 270
40             else ->
41                 throw UnsupportedOperationException("Unsupported display rotation: $rotationValue")
42         }
43 
44     /**
45      * Calculates the delta between a source rotation and destination rotation.
46      *
47      * <p>A typical use of this method would be calculating the angular difference between the
48      * display orientation (destRotationDegrees) and camera sensor orientation
49      * (sourceRotationDegrees).
50      *
51      * @param destRotationDegrees The destination rotation relative to the device's natural
52      *   rotation.
53      * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
54      * @param isOppositeFacing Whether the source and destination planes are facing opposite
55      *   directions.
56      */
57     @JvmStatic
calculateRelativeImageRotationDegreesnull58     fun calculateRelativeImageRotationDegrees(
59         destRotationDegrees: Int,
60         sourceRotationDegrees: Int,
61         isOppositeFacing: Boolean
62     ): Int =
63         if (isOppositeFacing) {
64             (sourceRotationDegrees - destRotationDegrees + 360) % 360
65         } else {
66             (sourceRotationDegrees + destRotationDegrees) % 360
67         }
68 
69     /**
70      * Calculates the transformation and applies it to the inner view of [TextureView] preview.
71      *
72      * [TextureView] needs a preliminary correction since it doesn't handle the display rotation.
73      */
74     @JvmStatic
transformTextureViewnull75     fun transformTextureView(
76         preview: TextureView,
77         containerViewSize: Size,
78         resolution: Size,
79         targetRotation: Int,
80         sensorRotationDegrees: Int,
81         isOppositeFacing: Boolean
82     ) {
83         // For TextureView, correct the orientation to match the target rotation.
84         preview.setTransform(getTextureViewCorrectionMatrix(resolution, targetRotation))
85 
86         val surfaceRectInPreview =
87             getTransformedSurfaceRect(
88                 containerViewSize,
89                 resolution,
90                 calculateRelativeImageRotationDegrees(
91                     surfaceRotationToRotationDegrees(targetRotation),
92                     sensorRotationDegrees,
93                     isOppositeFacing
94                 )
95             )
96 
97         preview.pivotX = 0f
98         preview.pivotY = 0f
99         preview.scaleX = surfaceRectInPreview.width() / resolution.width
100         preview.scaleY = surfaceRectInPreview.height() / resolution.height
101         preview.translationX = surfaceRectInPreview.left - preview.left
102         preview.translationY = surfaceRectInPreview.top - preview.top
103     }
104 
105     /**
106      * Creates a matrix that makes [TextureView]'s rotation matches the target rotation.
107      *
108      * The value should be applied by calling [TextureView.setTransform]. Usually the target
109      * rotation is the display rotation. In that case, this matrix will just make a [TextureView]
110      * works like a SurfaceView. If not, then it will further correct it to the desired rotation.
111      */
112     @JvmStatic
getTextureViewCorrectionMatrixnull113     private fun getTextureViewCorrectionMatrix(resolution: Size, targetRotation: Int): Matrix {
114         val surfaceRect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat())
115         val rotationDegrees = -surfaceRotationToRotationDegrees(targetRotation)
116         return getRectToRect(surfaceRect, surfaceRect, rotationDegrees)
117     }
118 
119     /**
120      * Gets the transform from one {@link Rect} to another with rotation degrees.
121      *
122      * <p> Following is how the source is mapped to the target with a 90° rotation. The rect <a, b,
123      * c, d> is mapped to <a', b', c', d'>.
124      * <pre>
125      *  a----------b               d'-----------a'
126      *  |  source  |    -90°->     |            |
127      *  d----------c               |   target   |
128      *                             |            |
129      *                             c'-----------b'
130      * </pre>
131      */
132     @JvmStatic
getRectToRectnull133     private fun getRectToRect(source: RectF, target: RectF, rotationDegrees: Int): Matrix =
134         Matrix().apply {
135             // Map source to normalized space.
136             setRectToRect(source, NORMALIZED_RECT, Matrix.ScaleToFit.FILL)
137             // Add rotation.
138             postRotate(rotationDegrees.toFloat())
139             // Restore the normalized space to target's coordinates.
140             postConcat(getNormalizedToBuffer(target))
141         }
142 
143     /** Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect. */
144     @JvmStatic
getNormalizedToBuffernull145     private fun getNormalizedToBuffer(viewPortRect: RectF): Matrix =
146         Matrix().apply { setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL) }
147 
148     /**
149      * Gets the transformed [Surface] rect in the preview coordinates.
150      *
151      * Returns desired rect of the inner view that once applied, the only part visible to end users
152      * is the crop rect.
153      */
154     @JvmStatic
getTransformedSurfaceRectnull155     private fun getTransformedSurfaceRect(
156         containerViewSize: Size,
157         resolution: Size,
158         rotationDegrees: Int
159     ): RectF {
160         val surfaceToPreviewMatrix =
161             getSurfaceToPreviewMatrix(containerViewSize, resolution, rotationDegrees)
162         val rect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat())
163         surfaceToPreviewMatrix.mapRect(rect)
164         return rect
165     }
166 
167     /**
168      * Calculates the transformation from [Surface] coordinates to the preview coordinates.
169      *
170      * The calculation is based on making the crop rect to center fill the preview.
171      */
172     @JvmStatic
getSurfaceToPreviewMatrixnull173     private fun getSurfaceToPreviewMatrix(
174         containerViewSize: Size,
175         resolution: Size,
176         rotationDegrees: Int
177     ): Matrix {
178         val surfaceRect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat())
179 
180         // Get the target of the mapping, the coordinates of the crop rect in the preview.
181         val previewCropRect =
182             getPreviewCropRect(containerViewSize, surfaceRect.toRect(), rotationDegrees)
183 
184         return getRectToRect(surfaceRect, previewCropRect, rotationDegrees)
185     }
186 
187     /** Gets the crop rect in the preview coordinates. */
188     @JvmStatic
getPreviewCropRectnull189     private fun getPreviewCropRect(
190         containerViewSize: Size,
191         surfaceCropRect: Rect,
192         rotationDegrees: Int
193     ): RectF {
194         val containerViewRect =
195             RectF(0f, 0f, containerViewSize.width.toFloat(), containerViewSize.height.toFloat())
196         val rotatedCropRectSize = getRotatedCropRectSize(surfaceCropRect, rotationDegrees)
197         val rotatedCropRect =
198             RectF(0f, 0f, rotatedCropRectSize.width.toFloat(), rotatedCropRectSize.height.toFloat())
199 
200         Matrix().apply {
201             // android.graphics.Matrix doesn't support fill scale types. The workaround is
202             // mapping inversely from destination to source, then invert the matrix.
203             setRectToRect(containerViewRect, rotatedCropRect, Matrix.ScaleToFit.CENTER)
204             invert(this)
205             mapRect(rotatedCropRect)
206         }
207 
208         return rotatedCropRect
209     }
210 
211     /** Returns crop rect size with target rotation applied. */
212     @JvmStatic
getRotatedCropRectSizenull213     private fun getRotatedCropRectSize(surfaceCropRect: Rect, rotationDegrees: Int): Size =
214         if (is90or270(rotationDegrees)) {
215             Size(surfaceCropRect.height(), surfaceCropRect.width())
216         } else Size(surfaceCropRect.width(), surfaceCropRect.height())
217 
218     /** Returns true if the rotation degrees is 90 or 270. */
219     @JvmStatic
is90or270null220     private fun is90or270(rotationDegrees: Int): Boolean {
221         if (rotationDegrees == 90 || rotationDegrees == 270) {
222             return true
223         }
224         if (rotationDegrees == 0 || rotationDegrees == 180) {
225             return false
226         }
227         throw IllegalArgumentException("Invalid rotation degrees: $rotationDegrees")
228     }
229 }
230