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