1 /*
2 * Copyright (C) 2023 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 // #define LOG_NDEBUG 0
17 #include "system/graphics.h"
18 #define LOG_TAG "JpegUtil"
19 #include <cstddef>
20 #include <cstdint>
21 #include <optional>
22 #include <vector>
23
24 #include "JpegUtil.h"
25 #include "android/hardware_buffer.h"
26 #include "jpeglib.h"
27 #include "log/log.h"
28 #include "ui/GraphicBuffer.h"
29 #include "ui/GraphicBufferMapper.h"
30 #include "util/Util.h"
31 #include "utils/Errors.h"
32
33 namespace android {
34 namespace companion {
35 namespace virtualcamera {
36 namespace {
37
38 constexpr int k2DCTSIZE = 2 * DCTSIZE;
39
40 class LibJpegContext {
41 public:
LibJpegContext(int width,int height,int quality,const size_t outBufferSize,void * outBuffer)42 LibJpegContext(int width, int height, int quality, const size_t outBufferSize,
43 void* outBuffer)
44 : mWidth(width),
45 mHeight(height),
46 mDstBufferSize(outBufferSize),
47 mDstBuffer(outBuffer) {
48 // Initialize error handling for libjpeg.
49 // We call jpeg_std_error to initialize standard error
50 // handling and then override:
51 // * output_message not to print to stderr, but use ALOG instead.
52 // * error_exit not to terminate the process, but failure flag instead.
53 mCompressStruct.err = jpeg_std_error(&mErrorMgr);
54 mCompressStruct.err->output_message = onOutputError;
55 mCompressStruct.err->error_exit = onErrorExit;
56 jpeg_create_compress(&mCompressStruct);
57
58 // Configure input image parameters.
59 mCompressStruct.image_width = width;
60 mCompressStruct.image_height = height;
61 mCompressStruct.input_components = 3;
62 mCompressStruct.in_color_space = JCS_YCbCr;
63 // We pass pointer to this instance as a client data so we can
64 // access this object from the static callbacks invoked by
65 // libjpeg.
66 mCompressStruct.client_data = this;
67
68 // Configure destination manager for libjpeg.
69 mCompressStruct.dest = &mDestinationMgr;
70 mDestinationMgr.init_destination = onInitDestination;
71 mDestinationMgr.empty_output_buffer = onEmptyOutputBuffer;
72 mDestinationMgr.term_destination = onTermDestination;
73 mDestinationMgr.next_output_byte = reinterpret_cast<JOCTET*>(mDstBuffer);
74 mDestinationMgr.free_in_buffer = mDstBufferSize;
75
76 // Configure everything else based on input configuration above.
77 jpeg_set_defaults(&mCompressStruct);
78
79 // Set quality and colorspace.
80 jpeg_set_quality(&mCompressStruct, quality, 1);
81 jpeg_set_colorspace(&mCompressStruct, JCS_YCbCr);
82
83 // Configure RAW input mode - this let's libjpeg know we're providing raw,
84 // subsampled YCbCr data.
85 mCompressStruct.raw_data_in = 1;
86 mCompressStruct.dct_method = JDCT_IFAST;
87
88 // Configure sampling factors - this states that every 2 Y
89 // samples share 1 Cb & 1 Cr component vertically & horizontally (YUV420).
90 mCompressStruct.comp_info[0].h_samp_factor = 2;
91 mCompressStruct.comp_info[0].v_samp_factor = 2;
92 mCompressStruct.comp_info[1].h_samp_factor = 1;
93 mCompressStruct.comp_info[1].v_samp_factor = 1;
94 mCompressStruct.comp_info[2].h_samp_factor = 1;
95 mCompressStruct.comp_info[2].v_samp_factor = 1;
96 }
97
setApp1Data(const uint8_t * app1Data,const size_t size)98 LibJpegContext& setApp1Data(const uint8_t* app1Data, const size_t size) {
99 mApp1Data = app1Data;
100 mApp1DataSize = size;
101 return *this;
102 }
103
compress(std::shared_ptr<AHardwareBuffer> inBuffer)104 std::optional<size_t> compress(std::shared_ptr<AHardwareBuffer> inBuffer) {
105 GraphicBuffer* gBuffer = GraphicBuffer::fromAHardwareBuffer(inBuffer.get());
106
107 if (gBuffer == nullptr) {
108 ALOGE("%s: Input graphic buffer is nullptr", __func__);
109 return std::nullopt;
110 }
111
112 if (gBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_YCbCr_420_888) {
113 // This should never happen since we're allocating the temporary buffer
114 // with YUV420 layout above.
115 ALOGE("%s: Cannot compress non-YUV buffer (pixelFormat %d)", __func__,
116 gBuffer->getPixelFormat());
117 return std::nullopt;
118 }
119
120 YCbCrLockGuard yCbCrLock(inBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN);
121 if (yCbCrLock.getStatus() != OK) {
122 ALOGE("%s: Failed to lock the input buffer: %s", __func__,
123 statusToString(yCbCrLock.getStatus()).c_str());
124 return std::nullopt;
125 }
126 const android_ycbcr& ycbr = *yCbCrLock;
127
128 const int inBufferWidth = gBuffer->getWidth();
129 const int inBufferHeight = gBuffer->getHeight();
130
131 if (inBufferWidth % k2DCTSIZE || (inBufferHeight % k2DCTSIZE)) {
132 ALOGE(
133 "%s: Compressing YUV420 buffer with size %dx%d not aligned with 2 * "
134 "DCTSIZE (%d) is not currently supported.",
135 __func__, inBufferWidth, inBufferHeight, DCTSIZE);
136 return std::nullopt;
137 }
138
139 if (inBufferWidth < mWidth || inBufferHeight < mHeight) {
140 ALOGE(
141 "%s: Input buffer has smaller size (%dx%d) than image to be "
142 "compressed (%dx%d)",
143 __func__, inBufferWidth, inBufferHeight, mWidth, mHeight);
144 return std::nullopt;
145 }
146
147 // Chroma planes have 1/2 resolution of the original image.
148 const int cHeight = inBufferHeight / 2;
149 const int cWidth = inBufferWidth / 2;
150
151 // Prepare arrays of pointers to scanlines of each plane.
152 std::vector<JSAMPROW> yLines(inBufferHeight);
153 std::vector<JSAMPROW> cbLines(cHeight);
154 std::vector<JSAMPROW> crLines(cHeight);
155
156 uint8_t* y = static_cast<uint8_t*>(ycbr.y);
157 uint8_t* cb = static_cast<uint8_t*>(ycbr.cb);
158 uint8_t* cr = static_cast<uint8_t*>(ycbr.cr);
159
160 // Since UV samples might be interleaved (semiplanar) we need to copy
161 // them to separate planes, since libjpeg doesn't directly
162 // support processing semiplanar YUV.
163 const int cSamples = cWidth * cHeight;
164 std::vector<uint8_t> cb_plane(cSamples);
165 std::vector<uint8_t> cr_plane(cSamples);
166
167 // TODO(b/301023410) - Use libyuv or ARM SIMD for "unzipping" the data.
168 int out_idx = 0;
169 for (int i = 0; i < cHeight; ++i) {
170 for (int j = 0; j < cWidth; ++j) {
171 cb_plane[out_idx] = cb[j * ycbr.chroma_step];
172 cr_plane[out_idx] = cr[j * ycbr.chroma_step];
173 out_idx++;
174 }
175 cb += ycbr.cstride;
176 cr += ycbr.cstride;
177 }
178
179 // Collect pointers to individual scanline of each plane.
180 for (int i = 0; i < inBufferHeight; ++i) {
181 yLines[i] = y + i * ycbr.ystride;
182 }
183 for (int i = 0; i < cHeight; ++i) {
184 cbLines[i] = cb_plane.data() + i * cWidth;
185 crLines[i] = cr_plane.data() + i * cWidth;
186 }
187
188 return compress(yLines, cbLines, crLines);
189 }
190
191 private:
setSuccess(const boolean success)192 void setSuccess(const boolean success) {
193 mSuccess = success;
194 }
195
initDestination()196 void initDestination() {
197 mDestinationMgr.next_output_byte = reinterpret_cast<JOCTET*>(mDstBuffer);
198 mDestinationMgr.free_in_buffer = mDstBufferSize;
199 ALOGV("%s:%d jpeg start: %p [%zu]", __FUNCTION__, __LINE__, mDstBuffer,
200 mDstBufferSize);
201 }
202
termDestination()203 void termDestination() {
204 mEncodedSize = mDstBufferSize - mDestinationMgr.free_in_buffer;
205 ALOGV("%s:%d Done with jpeg: %zu", __FUNCTION__, __LINE__, mEncodedSize);
206 }
207
208 // Perform actual compression.
209 //
210 // Takes vector of pointers to Y / Cb / Cr scanlines as an input. Length of
211 // each vector needs to correspond to height of corresponding plane.
212 //
213 // Returns size of compressed image in bytes on success, empty optional otherwise.
compress(std::vector<JSAMPROW> & yLines,std::vector<JSAMPROW> & cbLines,std::vector<JSAMPROW> & crLines)214 std::optional<size_t> compress(std::vector<JSAMPROW>& yLines,
215 std::vector<JSAMPROW>& cbLines,
216 std::vector<JSAMPROW>& crLines) {
217 jpeg_start_compress(&mCompressStruct, TRUE);
218
219 if (mApp1Data != nullptr && mApp1DataSize > 0) {
220 ALOGV("%s: Writing exif, size %zu B", __func__, mApp1DataSize);
221 jpeg_write_marker(&mCompressStruct, JPEG_APP0 + 1,
222 static_cast<const JOCTET*>(mApp1Data), mApp1DataSize);
223 }
224
225 while (mCompressStruct.next_scanline < mCompressStruct.image_height) {
226 const uint32_t batchSize = DCTSIZE * 2;
227 const uint32_t nl = mCompressStruct.next_scanline;
228 JSAMPARRAY planes[3]{&yLines[nl], &cbLines[nl / 2], &crLines[nl / 2]};
229
230 uint32_t done = jpeg_write_raw_data(&mCompressStruct, planes, batchSize);
231
232 if (done != batchSize) {
233 ALOGE("%s: compressed %u lines, expected %u (total %u/%u)",
234 __FUNCTION__, done, batchSize, mCompressStruct.next_scanline,
235 mCompressStruct.image_height);
236 return std::nullopt;
237 }
238 }
239 jpeg_finish_compress(&mCompressStruct);
240 return mEncodedSize;
241 }
242
243 // === libjpeg callbacks below ===
244
onOutputError(j_common_ptr cinfo)245 static void onOutputError(j_common_ptr cinfo) {
246 char buffer[JMSG_LENGTH_MAX];
247 (*cinfo->err->format_message)(cinfo, buffer);
248 ALOGE("libjpeg error: %s", buffer);
249 };
250
onErrorExit(j_common_ptr cinfo)251 static void onErrorExit(j_common_ptr cinfo) {
252 static_cast<LibJpegContext*>(cinfo->client_data)->setSuccess(false);
253 };
254
onInitDestination(j_compress_ptr cinfo)255 static void onInitDestination(j_compress_ptr cinfo) {
256 static_cast<LibJpegContext*>(cinfo->client_data)->initDestination();
257 }
258
onEmptyOutputBuffer(j_compress_ptr cinfo __unused)259 static int onEmptyOutputBuffer(j_compress_ptr cinfo __unused) {
260 ALOGV("%s:%d Out of buffer", __FUNCTION__, __LINE__);
261 return 0;
262 }
263
onTermDestination(j_compress_ptr cinfo)264 static void onTermDestination(j_compress_ptr cinfo) {
265 static_cast<LibJpegContext*>(cinfo->client_data)->termDestination();
266 }
267
268 jpeg_compress_struct mCompressStruct;
269 jpeg_error_mgr mErrorMgr;
270 jpeg_destination_mgr mDestinationMgr;
271
272 // APP1 data.
273 const uint8_t* mApp1Data = nullptr;
274 size_t mApp1DataSize = 0;
275
276 // Dimensions of the input image.
277 int mWidth;
278 int mHeight;
279
280 // Destination buffer and it's capacity.
281 size_t mDstBufferSize;
282 void* mDstBuffer;
283
284 // This will be set to size of encoded data
285 // written to the outputBuffer when encoding finishes.
286 size_t mEncodedSize;
287 // Set to true/false based on whether the encoding
288 // was successful.
289 boolean mSuccess = true;
290 };
291
roundTo2DCTMultiple(const int n)292 int roundTo2DCTMultiple(const int n) {
293 const int mod = n % k2DCTSIZE;
294 return mod == 0 ? n : n + (k2DCTSIZE - mod);
295 }
296
297 } // namespace
298
compressJpeg(const int width,const int height,const int quality,std::shared_ptr<AHardwareBuffer> inBuffer,const std::vector<uint8_t> & app1ExifData,size_t outBufferSize,void * outBuffer)299 std::optional<size_t> compressJpeg(const int width, const int height,
300 const int quality,
301 std::shared_ptr<AHardwareBuffer> inBuffer,
302 const std::vector<uint8_t>& app1ExifData,
303 size_t outBufferSize, void* outBuffer) {
304 LibJpegContext context(width, height, quality, outBufferSize, outBuffer);
305 if (!app1ExifData.empty()) {
306 context.setApp1Data(app1ExifData.data(), app1ExifData.size());
307 }
308 return context.compress(inBuffer);
309 }
310
roundTo2DctSize(const Resolution resolution)311 Resolution roundTo2DctSize(const Resolution resolution) {
312 return Resolution(roundTo2DCTMultiple(resolution.width),
313 roundTo2DCTMultiple(resolution.height));
314 }
315
316 } // namespace virtualcamera
317 } // namespace companion
318 } // namespace android
319