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