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