• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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