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