1 /* 2 * Copyright (C) 2010 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 com.android.graphics.flags.Flags; 20 21 import android.annotation.FlaggedApi; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import java.io.OutputStream; 25 26 /** 27 * YuvImage contains YUV data and provides a method that compresses a region of 28 * the YUV data to a Jpeg. The YUV data should be provided as a single byte 29 * array irrespective of the number of image planes in it. 30 * Currently only ImageFormat.NV21 and ImageFormat.YUY2 are supported. 31 * 32 * To compress a rectangle region in the YUV data, users have to specify the 33 * region by left, top, width and height. 34 */ 35 @android.ravenwood.annotation.RavenwoodKeepWholeClass 36 public class YuvImage { 37 38 /** 39 * Number of bytes of temp storage we use for communicating between the 40 * native compressor and the java OutputStream. 41 */ 42 private final static int WORKING_COMPRESS_STORAGE = 4096; 43 44 /** 45 * The YUV format as defined in {@link ImageFormat}. 46 */ 47 private int mFormat; 48 49 /** 50 * The raw YUV data. 51 * In the case of more than one image plane, the image planes must be 52 * concatenated into a single byte array. 53 */ 54 private byte[] mData; 55 56 /** 57 * The number of row bytes in each image plane. 58 */ 59 private int[] mStrides; 60 61 /** 62 * The width of the image. 63 */ 64 private int mWidth; 65 66 /** 67 * The height of the image. 68 */ 69 private int mHeight; 70 71 /** 72 * The color space of the image, defaults to SRGB 73 */ 74 @NonNull private ColorSpace mColorSpace; 75 76 /** 77 * Array listing all supported ImageFormat that are supported by this class 78 */ 79 private final static String[] sSupportedFormats = 80 {"NV21", "YUY2", "YCBCR_P010", "YUV_420_888"}; 81 printSupportedFormats()82 private static String printSupportedFormats() { 83 StringBuilder sb = new StringBuilder(); 84 for (int i = 0; i < sSupportedFormats.length; ++i) { 85 sb.append(sSupportedFormats[i]); 86 if (i != sSupportedFormats.length - 1) { 87 sb.append(", "); 88 } 89 } 90 return sb.toString(); 91 } 92 93 /** 94 * Array listing all supported HDR ColorSpaces that are supported by JPEG/R encoding 95 */ 96 private final static ColorSpace.Named[] sSupportedJpegRHdrColorSpaces = { 97 ColorSpace.Named.BT2020_HLG, 98 ColorSpace.Named.BT2020_PQ 99 }; 100 101 /** 102 * Array listing all supported SDR ColorSpaces that are supported by JPEG/R encoding 103 */ 104 private final static ColorSpace.Named[] sSupportedJpegRSdrColorSpaces = { 105 ColorSpace.Named.SRGB, 106 ColorSpace.Named.DISPLAY_P3 107 }; 108 printSupportedJpegRColorSpaces(boolean isHdr)109 private static String printSupportedJpegRColorSpaces(boolean isHdr) { 110 ColorSpace.Named[] colorSpaces = isHdr ? sSupportedJpegRHdrColorSpaces : 111 sSupportedJpegRSdrColorSpaces; 112 StringBuilder sb = new StringBuilder(); 113 for (int i = 0; i < colorSpaces.length; ++i) { 114 sb.append(ColorSpace.get(colorSpaces[i]).getName()); 115 if (i != colorSpaces.length - 1) { 116 sb.append(", "); 117 } 118 } 119 return sb.toString(); 120 } 121 isSupportedJpegRColorSpace(boolean isHdr, int colorSpace)122 private static boolean isSupportedJpegRColorSpace(boolean isHdr, int colorSpace) { 123 ColorSpace.Named[] colorSpaces = isHdr ? sSupportedJpegRHdrColorSpaces : 124 sSupportedJpegRSdrColorSpaces; 125 for (ColorSpace.Named cs : colorSpaces) { 126 if (cs.ordinal() == colorSpace) { 127 return true; 128 } 129 } 130 return false; 131 } 132 133 134 /** 135 * Construct an YuvImage. Use SRGB for as default {@link ColorSpace}. 136 * 137 * @param yuv The YUV data. In the case of more than one image plane, all the planes must be 138 * concatenated into a single byte array. 139 * @param format The YUV data format as defined in {@link ImageFormat}. 140 * @param width The width of the YuvImage. 141 * @param height The height of the YuvImage. 142 * @param strides (Optional) Row bytes of each image plane. If yuv contains padding, the stride 143 * of each image must be provided. If strides is null, the method assumes no 144 * padding and derives the row bytes by format and width itself. 145 * @throws IllegalArgumentException if format is not support; width or height <= 0; or yuv is 146 * null. 147 */ YuvImage(byte[] yuv, int format, int width, int height, int[] strides)148 public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) { 149 this(yuv, format, width, height, strides, ColorSpace.get(ColorSpace.Named.SRGB)); 150 } 151 152 /** 153 * Construct an YuvImage. 154 * 155 * @param yuv The YUV data. In the case of more than one image plane, all the planes 156 * must be concatenated into a single byte array. 157 * @param format The YUV data format as defined in {@link ImageFormat}. 158 * @param width The width of the YuvImage. 159 * @param height The height of the YuvImage. 160 * @param strides (Optional) Row bytes of each image plane. If yuv contains padding, the 161 * stride of each image must be provided. If strides is null, the method 162 * assumes no padding and derives the row bytes by format and width itself. 163 * @param colorSpace The YUV image color space as defined in {@link ColorSpace}. 164 * If the parameter is null, SRGB will be set as the default value. 165 * @throws IllegalArgumentException if format is not support; width or height <= 0; or yuv is 166 * null. 167 */ YuvImage(@onNull byte[] yuv, int format, int width, int height, @Nullable int[] strides, @NonNull ColorSpace colorSpace)168 public YuvImage(@NonNull byte[] yuv, int format, int width, int height, 169 @Nullable int[] strides, @NonNull ColorSpace colorSpace) { 170 if (format != ImageFormat.NV21 && 171 format != ImageFormat.YUY2 && 172 format != ImageFormat.YCBCR_P010 && 173 format != ImageFormat.YUV_420_888) { 174 throw new IllegalArgumentException( 175 "only supports the following ImageFormat:" + printSupportedFormats()); 176 } 177 178 if (width <= 0 || height <= 0) { 179 throw new IllegalArgumentException( 180 "width and height must large than 0"); 181 } 182 183 if (yuv == null) { 184 throw new IllegalArgumentException("yuv cannot be null"); 185 } 186 187 if (colorSpace == null) { 188 throw new IllegalArgumentException("ColorSpace cannot be null"); 189 } 190 191 if (strides == null) { 192 mStrides = calculateStrides(width, format); 193 } else { 194 mStrides = strides; 195 } 196 197 mData = yuv; 198 mFormat = format; 199 mWidth = width; 200 mHeight = height; 201 mColorSpace = colorSpace; 202 } 203 204 /** 205 * Compress a rectangle region in the YuvImage to a jpeg. 206 * For image format, only ImageFormat.NV21 and ImageFormat.YUY2 are supported. 207 * For color space, only SRGB is supported. 208 * 209 * @param rectangle The rectangle region to be compressed. The medthod checks if rectangle is 210 * inside the image. Also, the method modifies rectangle if the chroma pixels 211 * in it are not matched with the luma pixels in it. 212 * @param quality Hint to the compressor, 0-100. 0 meaning compress for 213 * small size, 100 meaning compress for max quality. 214 * @param stream OutputStream to write the compressed data. 215 * @return True if the compression is successful. 216 * @throws IllegalArgumentException if rectangle is invalid; color space or image format 217 * is not supported; quality is not within [0, 100]; or stream is null. 218 */ compressToJpeg(Rect rectangle, int quality, OutputStream stream)219 public boolean compressToJpeg(Rect rectangle, int quality, OutputStream stream) { 220 if (mFormat != ImageFormat.NV21 && mFormat != ImageFormat.YUY2) { 221 throw new IllegalArgumentException( 222 "Only ImageFormat.NV21 and ImageFormat.YUY2 are supported."); 223 } 224 if (mColorSpace.getId() != ColorSpace.Named.SRGB.ordinal()) { 225 throw new IllegalArgumentException("Only SRGB color space is supported."); 226 } 227 228 Rect wholeImage = new Rect(0, 0, mWidth, mHeight); 229 if (!wholeImage.contains(rectangle)) { 230 throw new IllegalArgumentException( 231 "rectangle is not inside the image"); 232 } 233 234 if (quality < 0 || quality > 100) { 235 throw new IllegalArgumentException("quality must be 0..100"); 236 } 237 238 if (stream == null) { 239 throw new IllegalArgumentException("stream cannot be null"); 240 } 241 242 adjustRectangle(rectangle); 243 int[] offsets = calculateOffsets(rectangle.left, rectangle.top); 244 245 return nativeCompressToJpeg(mData, mFormat, rectangle.width(), 246 rectangle.height(), offsets, mStrides, quality, stream, 247 new byte[WORKING_COMPRESS_STORAGE]); 248 } 249 250 /** 251 * Compress the HDR image into JPEG/R format. 252 * 253 * Sample usage: 254 * hdr_image.compressToJpegR(sdr_image, 90, stream); 255 * 256 * For the SDR image, only YUV_420_888 image format is supported, and the following 257 * color spaces are supported: 258 * ColorSpace.Named.SRGB, 259 * ColorSpace.Named.DISPLAY_P3 260 * 261 * For the HDR image, only YCBCR_P010 image format is supported, and the following 262 * color spaces are supported: 263 * ColorSpace.Named.BT2020_HLG, 264 * ColorSpace.Named.BT2020_PQ 265 * 266 * @param sdr The SDR image, only ImageFormat.YUV_420_888 is supported. 267 * @param quality Hint to the compressor, 0-100. 0 meaning compress for 268 * small size, 100 meaning compress for max quality. 269 * @param stream OutputStream to write the compressed data. 270 * @return True if the compression is successful. 271 * @throws IllegalArgumentException if input images are invalid; quality is not within [0, 272 * 100]; or stream is null. 273 */ compressToJpegR(@onNull YuvImage sdr, int quality, @NonNull OutputStream stream)274 public boolean compressToJpegR(@NonNull YuvImage sdr, int quality, 275 @NonNull OutputStream stream) { 276 byte[] emptyExif = new byte[0]; 277 return compressToJpegR(sdr, quality, stream, emptyExif); 278 } 279 280 /** 281 * Compress the HDR image into JPEG/R format. 282 * 283 * Sample usage: 284 * hdr_image.compressToJpegR(sdr_image, 90, stream); 285 * 286 * For the SDR image, only YUV_420_888 image format is supported, and the following 287 * color spaces are supported: 288 * ColorSpace.Named.SRGB, 289 * ColorSpace.Named.DISPLAY_P3 290 * 291 * For the HDR image, only YCBCR_P010 image format is supported, and the following 292 * color spaces are supported: 293 * ColorSpace.Named.BT2020_HLG, 294 * ColorSpace.Named.BT2020_PQ 295 * 296 * @param sdr The SDR image, only ImageFormat.YUV_420_888 is supported. 297 * @param quality Hint to the compressor, 0-100. 0 meaning compress for 298 * small size, 100 meaning compress for max quality. 299 * @param stream OutputStream to write the compressed data. 300 * @param exif Exchangeable image file format. 301 * @return True if the compression is successful. 302 * @throws IllegalArgumentException if input images are invalid; quality is not within [0, 303 * 100]; or stream is null. 304 */ 305 @FlaggedApi(Flags.FLAG_YUV_IMAGE_COMPRESS_TO_ULTRA_HDR) compressToJpegR(@onNull YuvImage sdr, int quality, @NonNull OutputStream stream, @NonNull byte[] exif)306 public boolean compressToJpegR(@NonNull YuvImage sdr, int quality, 307 @NonNull OutputStream stream, @NonNull byte[] exif) { 308 if (sdr == null) { 309 throw new IllegalArgumentException("SDR input cannot be null"); 310 } 311 312 if (mData.length == 0 || sdr.getYuvData().length == 0) { 313 throw new IllegalArgumentException("Input images cannot be empty"); 314 } 315 316 if (mFormat != ImageFormat.YCBCR_P010 || sdr.getYuvFormat() != ImageFormat.YUV_420_888) { 317 throw new IllegalArgumentException( 318 "only support ImageFormat.YCBCR_P010 and ImageFormat.YUV_420_888"); 319 } 320 321 if (sdr.getWidth() != mWidth || sdr.getHeight() != mHeight) { 322 throw new IllegalArgumentException("HDR and SDR resolution mismatch"); 323 } 324 325 if (quality < 0 || quality > 100) { 326 throw new IllegalArgumentException("quality must be 0..100"); 327 } 328 329 if (stream == null) { 330 throw new IllegalArgumentException("stream cannot be null"); 331 } 332 333 if (!isSupportedJpegRColorSpace(true, mColorSpace.getId()) || 334 !isSupportedJpegRColorSpace(false, sdr.getColorSpace().getId())) { 335 throw new IllegalArgumentException("Not supported color space. " 336 + "SDR only supports: " + printSupportedJpegRColorSpaces(false) 337 + "HDR only supports: " + printSupportedJpegRColorSpaces(true)); 338 } 339 340 return nativeCompressToJpegR(mData, mColorSpace.getDataSpace(), 341 sdr.getYuvData(), sdr.getColorSpace().getDataSpace(), 342 mWidth, mHeight, quality, stream, 343 new byte[WORKING_COMPRESS_STORAGE], exif, 344 mStrides, sdr.getStrides()); 345 } 346 347 348 /** 349 * @return the YUV data. 350 */ getYuvData()351 public byte[] getYuvData() { 352 return mData; 353 } 354 355 /** 356 * @return the YUV format as defined in {@link ImageFormat}. 357 */ getYuvFormat()358 public int getYuvFormat() { 359 return mFormat; 360 } 361 362 /** 363 * @return the number of row bytes in each image plane. 364 */ getStrides()365 public int[] getStrides() { 366 return mStrides; 367 } 368 369 /** 370 * @return the width of the image. 371 */ getWidth()372 public int getWidth() { 373 return mWidth; 374 } 375 376 /** 377 * @return the height of the image. 378 */ getHeight()379 public int getHeight() { 380 return mHeight; 381 } 382 383 384 /** 385 * @return the color space of the image. 386 */ getColorSpace()387 public @NonNull ColorSpace getColorSpace() { return mColorSpace; } 388 calculateOffsets(int left, int top)389 int[] calculateOffsets(int left, int top) { 390 int[] offsets = null; 391 if (mFormat == ImageFormat.NV21) { 392 offsets = new int[] {top * mStrides[0] + left, 393 mHeight * mStrides[0] + top / 2 * mStrides[1] 394 + left / 2 * 2 }; 395 return offsets; 396 } 397 398 if (mFormat == ImageFormat.YUY2) { 399 offsets = new int[] {top * mStrides[0] + left / 2 * 4}; 400 return offsets; 401 } 402 403 return offsets; 404 } 405 calculateStrides(int width, int format)406 private int[] calculateStrides(int width, int format) { 407 int[] strides = null; 408 switch (format) { 409 case ImageFormat.NV21: 410 strides = new int[] {width, width}; 411 return strides; 412 case ImageFormat.YCBCR_P010: 413 strides = new int[] {width * 2, width * 2}; 414 return strides; 415 case ImageFormat.YUV_420_888: 416 strides = new int[] {width, (width + 1) / 2, (width + 1) / 2}; 417 return strides; 418 case ImageFormat.YUY2: 419 strides = new int[] {width * 2}; 420 return strides; 421 default: 422 throw new IllegalArgumentException( 423 "only supports the following ImageFormat:" + printSupportedFormats()); 424 } 425 } 426 adjustRectangle(Rect rect)427 private void adjustRectangle(Rect rect) { 428 int width = rect.width(); 429 int height = rect.height(); 430 if (mFormat == ImageFormat.NV21) { 431 // Make sure left, top, width and height are all even. 432 width &= ~1; 433 height &= ~1; 434 rect.left &= ~1; 435 rect.top &= ~1; 436 rect.right = rect.left + width; 437 rect.bottom = rect.top + height; 438 } 439 440 if (mFormat == ImageFormat.YUY2) { 441 // Make sure left and width are both even. 442 width &= ~1; 443 rect.left &= ~1; 444 rect.right = rect.left + width; 445 } 446 } 447 448 //////////// native methods 449 nativeCompressToJpeg(byte[] oriYuv, int format, int width, int height, int[] offsets, int[] strides, int quality, OutputStream stream, byte[] tempStorage)450 private static native boolean nativeCompressToJpeg(byte[] oriYuv, 451 int format, int width, int height, int[] offsets, int[] strides, 452 int quality, OutputStream stream, byte[] tempStorage); 453 nativeCompressToJpegR(byte[] hdr, int hdrColorSpaceId, byte[] sdr, int sdrColorSpaceId, int width, int height, int quality, OutputStream stream, byte[] tempStorage, byte[] exif, int[] hdrStrides, int[] sdrStrides)454 private static native boolean nativeCompressToJpegR(byte[] hdr, int hdrColorSpaceId, 455 byte[] sdr, int sdrColorSpaceId, int width, int height, int quality, 456 OutputStream stream, byte[] tempStorage, byte[] exif, 457 int[] hdrStrides, int[] sdrStrides); 458 } 459