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