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