1 /*
2  * Copyright (C) 2023 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 android.graphics;
18 
19 import android.annotation.FlaggedApi;
20 import android.annotation.FloatRange;
21 import android.annotation.NonNull;
22 import android.os.Parcel;
23 import android.os.Parcelable;
24 
25 import com.android.graphics.hwui.flags.Flags;
26 
27 import libcore.util.NativeAllocationRegistry;
28 
29 /**
30  * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable
31  * display adjustment capability. It is a combination of a set of metadata describing how to apply
32  * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3
33  * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored)
34  * channel Bitmap that represents the gainmap data itself.
35  * <p>
36  * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the
37  * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient
38  * HDR headroom is available.
39  *
40  * <h3>Gainmap Structure</h3>
41  *
42  * The logical whole of a gainmap'd image consists of a base Bitmap that represents the original
43  * image as would be displayed without gainmap support in addition to a gainmap with a second
44  * enhancement image. In the case of a JPEG, the base image would be the typical 8-bit SDR image
45  * that the format is commonly associated with. The gainmap image is embedded alongside the base
46  * image, often at a lower resolution (such as 1/4th), along with some metadata to describe
47  * how to apply the gainmap. The gainmap image itself is then a greyscale image representing
48  * the transformation to apply onto the base image to reconstruct an HDR rendition of it.
49  * <p>
50  * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a
51  * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains
52  * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()}
53  *
54  * <h3>Applying a gainmap manually</h3>
55  *
56  * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not
57  * automatically applied. In such situations, the following steps are appropriate to render the
58  * gainmap in combination with the base image.
59  * <p>
60  * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on
61  * this display. Let B be the pixel value from the base image in a color space that has the
62  * primaries of the base image and a linear transfer function. Let G be the pixel value from the
63  * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed
64  * as follows:
65  * <p>
66  * First, let W be a weight parameter determining how much the gainmap will be applied.
67  * <pre class="prettyprint">
68  *   W = clamp((log(H)                      - log(minDisplayRatioForHdrTransition)) /
69  *             (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre>
70  *
71  * Next, let L be the gainmap value in log space. We compute this from the value G that was
72  * sampled from the texture as follows:
73  * <pre class="prettyprint">
74  *   L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre>
75  * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then
76  * compute:
77  * <pre class="prettyprint">
78  *   D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre>
79  * <p>
80  * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base
81  * for these functions cancels out and does not affect the result, so other bases may be used
82  * if preferred.
83  */
84 public final class Gainmap implements Parcelable {
85 
86     // Use a Holder to allow static initialization of Gainmap in the boot image.
87     private static class NoImagePreloadHolder {
88         public static final NativeAllocationRegistry sRegistry =
89                 NativeAllocationRegistry.createMalloced(
90                         Gainmap.class.getClassLoader(), nGetFinalizer());
91     }
92 
93     final long mNativePtr;
94     private Bitmap mGainmapContents;
95 
96     // called from JNI
Gainmap(Bitmap gainmapContents, long nativeGainmap)97     private Gainmap(Bitmap gainmapContents, long nativeGainmap) {
98         if (nativeGainmap == 0) {
99             throw new RuntimeException("internal error: native gainmap is 0");
100         }
101 
102         mNativePtr = nativeGainmap;
103         setGainmapContents(gainmapContents);
104 
105         NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, nativeGainmap);
106     }
107 
108     /**
109      * Creates a gainmap from a given Bitmap. The caller is responsible for setting the various
110      * fields to the desired values. The defaults are as follows:
111      * <ul>
112      *     <li>Ratio min is 1f, 1f, 1f</li>
113      *     <li>Ratio max is 2f, 2f, 2f</li>
114      *     <li>Gamma is 1f, 1f, 1f</li>
115      *     <li>Epsilon SDR is 0f, 0f, 0f</li>
116      *     <li>Epsilon HDR is 0f, 0f, 0f</li>
117      *     <li>Display ratio SDR is 1f</li>
118      *     <li>Display ratio HDR is 2f</li>
119      * </ul>
120      * It is strongly recommended that at least the ratio max and display ratio HDR are adjusted
121      * to better suit the given gainmap contents.
122      */
Gainmap(@onNull Bitmap gainmapContents)123     public Gainmap(@NonNull Bitmap gainmapContents) {
124         this(gainmapContents, nCreateEmpty());
125     }
126 
127     /**
128      * Creates a new gainmap using the provided gainmap as the metadata source and the provided
129      * bitmap as the replacement for the gainmapContents
130      */
131     @FlaggedApi(Flags.FLAG_GAINMAP_CONSTRUCTOR_WITH_METADATA)
Gainmap(@onNull Gainmap gainmap, @NonNull Bitmap gainmapContents)132     public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) {
133         this(gainmapContents, nCreateCopy(gainmap.mNativePtr));
134     }
135 
136     /**
137      * @return Returns the image data of the gainmap represented as a Bitmap. This is represented
138      * as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored
139      * such as {@link Bitmap#getColorSpace()} or {@link Bitmap#getGainmap()} as they are not
140      * relevant to the gainmap's enhancement layer.
141      */
142     @NonNull
getGainmapContents()143     public Bitmap getGainmapContents() {
144         return mGainmapContents;
145     }
146 
147     /**
148      * Sets the image data of the gainmap. This is the 1 or 3 channel enhancement layer to apply
149      * to the base image. This is represented as a Bitmap for broad API compatibility, however
150      * certain aspects of the Bitmap are ignored such as {@link Bitmap#getColorSpace()} or
151      * {@link Bitmap#getGainmap()} as they are not relevant to the gainmap's enhancement layer.
152      *
153      * @param bitmap The non-null bitmap to set as the gainmap's contents
154      */
setGainmapContents(@onNull Bitmap bitmap)155     public void setGainmapContents(@NonNull Bitmap bitmap) {
156         // TODO: Validate here or leave native-side?
157         if (bitmap.isRecycled()) throw new IllegalArgumentException("Bitmap is recycled");
158         nSetBitmap(mNativePtr, bitmap);
159         mGainmapContents = bitmap;
160     }
161 
162     /**
163      * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same.
164      */
setRatioMin(float r, float g, float b)165     public void setRatioMin(float r, float g, float b) {
166         nSetRatioMin(mNativePtr, r, g, b);
167     }
168 
169     /**
170      * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the
171      * same. The components are in r, g, b order.
172      */
173     @NonNull
getRatioMin()174     public float[] getRatioMin() {
175         float[] ret = new float[3];
176         nGetRatioMin(mNativePtr, ret);
177         return ret;
178     }
179 
180     /**
181      * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same.
182      */
setRatioMax(float r, float g, float b)183     public void setRatioMax(float r, float g, float b) {
184         nSetRatioMax(mNativePtr, r, g, b);
185     }
186 
187     /**
188      * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the
189      * same. The components are in r, g, b order.
190      */
191     @NonNull
getRatioMax()192     public float[] getRatioMax() {
193         float[] ret = new float[3];
194         nGetRatioMax(mNativePtr, ret);
195         return ret;
196     }
197 
198     /**
199      * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same.
200      */
setGamma(float r, float g, float b)201     public void setGamma(float r, float g, float b) {
202         nSetGamma(mNativePtr, r, g, b);
203     }
204 
205     /**
206      * Gets the gainmap gamma. For single-plane gainmaps, all 3 components should be the
207      * same. The components are in r, g, b order.
208      */
209     @NonNull
getGamma()210     public float[] getGamma() {
211         float[] ret = new float[3];
212         nGetGamma(mNativePtr, ret);
213         return ret;
214     }
215 
216     /**
217      * Sets the sdr epsilon which is used to avoid numerical instability.
218      * For single-plane gainmaps, r, g, and b should be the same.
219      */
setEpsilonSdr(float r, float g, float b)220     public void setEpsilonSdr(float r, float g, float b) {
221         nSetEpsilonSdr(mNativePtr, r, g, b);
222     }
223 
224     /**
225      * Gets the sdr epsilon. For single-plane gainmaps, all 3 components should be the
226      * same. The components are in r, g, b order.
227      */
228     @NonNull
getEpsilonSdr()229     public float[] getEpsilonSdr() {
230         float[] ret = new float[3];
231         nGetEpsilonSdr(mNativePtr, ret);
232         return ret;
233     }
234 
235     /**
236      * Sets the hdr epsilon which is used to avoid numerical instability.
237      * For single-plane gainmaps, r, g, and b should be the same.
238      */
setEpsilonHdr(float r, float g, float b)239     public void setEpsilonHdr(float r, float g, float b) {
240         nSetEpsilonHdr(mNativePtr, r, g, b);
241     }
242 
243     /**
244      * Gets the hdr epsilon. For single-plane gainmaps, all 3 components should be the
245      * same. The components are in r, g, b order.
246      */
247     @NonNull
getEpsilonHdr()248     public float[] getEpsilonHdr() {
249         float[] ret = new float[3];
250         nGetEpsilonHdr(mNativePtr, ret);
251         return ret;
252     }
253 
254     /**
255      * Sets the hdr/sdr ratio at which point the gainmap is fully applied.
256      * @param max The hdr/sdr ratio at which the gainmap is fully applied. Must be >= 1.0f
257      */
setDisplayRatioForFullHdr(@loatRangefrom = 1.0f) float max)258     public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) {
259         if (!Float.isFinite(max) || max < 1f) {
260             throw new IllegalArgumentException(
261                     "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max);
262         }
263         nSetDisplayRatioHdr(mNativePtr, max);
264     }
265 
266     /**
267      * Gets the hdr/sdr ratio at which point the gainmap is fully applied.
268      */
269     @NonNull
getDisplayRatioForFullHdr()270     public float getDisplayRatioForFullHdr() {
271         return nGetDisplayRatioHdr(mNativePtr);
272     }
273 
274     /**
275      * Sets the hdr/sdr ratio below which only the SDR image is displayed.
276      * @param min The minimum hdr/sdr ratio at which to begin applying the gainmap. Must be >= 1.0f
277      */
setMinDisplayRatioForHdrTransition(@loatRangefrom = 1.0f) float min)278     public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) {
279         if (!Float.isFinite(min) || min < 1f) {
280             throw new IllegalArgumentException(
281                     "setMinDisplayRatioForHdrTransition must be >= 1.0f, got = " + min);
282         }
283         nSetDisplayRatioSdr(mNativePtr, min);
284     }
285 
286     /**
287      * Gets the hdr/sdr ratio below which only the SDR image is displayed.
288      */
289     @NonNull
getMinDisplayRatioForHdrTransition()290     public float getMinDisplayRatioForHdrTransition() {
291         return nGetDisplayRatioSdr(mNativePtr);
292     }
293 
294     /**
295      * No special parcel contents.
296      */
297     @Override
describeContents()298     public int describeContents() {
299         return 0;
300     }
301 
302     /**
303      * Write the gainmap to the parcel.
304      *
305      * @param dest Parcel object to write the gainmap data into
306      * @param flags Additional flags about how the object should be written.
307      */
308     @Override
writeToParcel(@onNull Parcel dest, int flags)309     public void writeToParcel(@NonNull Parcel dest, int flags) {
310         if (mNativePtr == 0) {
311             throw new IllegalStateException("Cannot be written to a parcel");
312         }
313         dest.writeTypedObject(mGainmapContents, flags);
314         // write gainmapinfo into parcel
315         nWriteGainmapToParcel(mNativePtr, dest);
316     }
317 
318     public static final @NonNull Parcelable.Creator<Gainmap> CREATOR =
319             new Parcelable.Creator<Gainmap>() {
320             /**
321              * Rebuilds a gainmap previously stored with writeToParcel().
322              *
323              * @param in Parcel object to read the gainmap from
324              * @return a new gainmap created from the data in the parcel
325              */
326             public Gainmap createFromParcel(Parcel in) {
327                 Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR));
328                 // read gainmapinfo from parcel
329                 nReadGainmapFromParcel(gm.mNativePtr, in);
330                 return gm;
331             }
332 
333             public Gainmap[] newArray(int size) {
334                 return new Gainmap[size];
335             }
336         };
337 
nGetFinalizer()338     private static native long nGetFinalizer();
nCreateEmpty()339     private static native long nCreateEmpty();
nCreateCopy(long source)340     private static native long nCreateCopy(long source);
341 
nSetBitmap(long ptr, Bitmap bitmap)342     private static native void nSetBitmap(long ptr, Bitmap bitmap);
343 
nSetRatioMin(long ptr, float r, float g, float b)344     private static native void nSetRatioMin(long ptr, float r, float g, float b);
nGetRatioMin(long ptr, float[] components)345     private static native void nGetRatioMin(long ptr, float[] components);
346 
nSetRatioMax(long ptr, float r, float g, float b)347     private static native void nSetRatioMax(long ptr, float r, float g, float b);
nGetRatioMax(long ptr, float[] components)348     private static native void nGetRatioMax(long ptr, float[] components);
349 
nSetGamma(long ptr, float r, float g, float b)350     private static native void nSetGamma(long ptr, float r, float g, float b);
nGetGamma(long ptr, float[] components)351     private static native void nGetGamma(long ptr, float[] components);
352 
nSetEpsilonSdr(long ptr, float r, float g, float b)353     private static native void nSetEpsilonSdr(long ptr, float r, float g, float b);
nGetEpsilonSdr(long ptr, float[] components)354     private static native void nGetEpsilonSdr(long ptr, float[] components);
355 
nSetEpsilonHdr(long ptr, float r, float g, float b)356     private static native void nSetEpsilonHdr(long ptr, float r, float g, float b);
nGetEpsilonHdr(long ptr, float[] components)357     private static native void nGetEpsilonHdr(long ptr, float[] components);
358 
nSetDisplayRatioHdr(long ptr, float max)359     private static native void nSetDisplayRatioHdr(long ptr, float max);
nGetDisplayRatioHdr(long ptr)360     private static native float nGetDisplayRatioHdr(long ptr);
361 
nSetDisplayRatioSdr(long ptr, float min)362     private static native void nSetDisplayRatioSdr(long ptr, float min);
nGetDisplayRatioSdr(long ptr)363     private static native float nGetDisplayRatioSdr(long ptr);
nWriteGainmapToParcel(long ptr, Parcel dest)364     private static native void nWriteGainmapToParcel(long ptr, Parcel dest);
nReadGainmapFromParcel(long ptr, Parcel src)365     private static native void nReadGainmapFromParcel(long ptr, Parcel src);
366 }
367