From 761b52d1d4d3fc75eb4dcd66766c8c465ea96099 Mon Sep 17 00:00:00 2001 From: Dichen Zhang Date: Fri, 10 Feb 2023 22:38:38 +0000 Subject: JpegR refactor: rename recoverymap to jpegr Bug: 264715926 Test: build Change-Id: I6fd866979bcb608084293a16c59f5ce150dbff12 --- libs/jpegrecoverymap/Android.bp | 4 +- .../include/jpegrecoverymap/jpegr.h | 328 +++++++ .../include/jpegrecoverymap/jpegrutils.h | 97 +++ .../include/jpegrecoverymap/recoverymap.h | 331 ------- .../include/jpegrecoverymap/recoverymapmath.h | 2 +- .../include/jpegrecoverymap/recoverymaputils.h | 97 --- libs/jpegrecoverymap/jpegr.cpp | 957 +++++++++++++++++++++ libs/jpegrecoverymap/jpegrutils.cpp | 250 ++++++ libs/jpegrecoverymap/recoverymap.cpp | 957 --------------------- libs/jpegrecoverymap/recoverymaputils.cpp | 250 ------ libs/jpegrecoverymap/tests/Android.bp | 2 +- libs/jpegrecoverymap/tests/jpegr_test.cpp | 499 +++++++++++ libs/jpegrecoverymap/tests/recoverymap_test.cpp | 499 ----------- 13 files changed, 2135 insertions(+), 2138 deletions(-) create mode 100644 libs/jpegrecoverymap/include/jpegrecoverymap/jpegr.h create mode 100644 libs/jpegrecoverymap/include/jpegrecoverymap/jpegrutils.h delete mode 100644 libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h delete mode 100644 libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h create mode 100644 libs/jpegrecoverymap/jpegr.cpp create mode 100644 libs/jpegrecoverymap/jpegrutils.cpp delete mode 100644 libs/jpegrecoverymap/recoverymap.cpp delete mode 100644 libs/jpegrecoverymap/recoverymaputils.cpp create mode 100644 libs/jpegrecoverymap/tests/jpegr_test.cpp delete mode 100644 libs/jpegrecoverymap/tests/recoverymap_test.cpp diff --git a/libs/jpegrecoverymap/Android.bp b/libs/jpegrecoverymap/Android.bp index ee112e802c..78d1912d24 100644 --- a/libs/jpegrecoverymap/Android.bp +++ b/libs/jpegrecoverymap/Android.bp @@ -29,9 +29,9 @@ cc_library { local_include_dirs: ["include"], srcs: [ - "recoverymap.cpp", + "jpegr.cpp", "recoverymapmath.cpp", - "recoverymaputils.cpp", + "jpegrutils.cpp", ], shared_libs: [ diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/jpegr.h b/libs/jpegrecoverymap/include/jpegrecoverymap/jpegr.h new file mode 100644 index 0000000000..9f8801e1bb --- /dev/null +++ b/libs/jpegrecoverymap/include/jpegrecoverymap/jpegr.h @@ -0,0 +1,328 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_JPEGRECOVERYMAP_JPEGR_H +#define ANDROID_JPEGRECOVERYMAP_JPEGR_H + +#include "jpegrerrorcode.h" + +namespace android::recoverymap { + +// Color gamuts for image data +typedef enum { + JPEGR_COLORGAMUT_UNSPECIFIED, + JPEGR_COLORGAMUT_BT709, + JPEGR_COLORGAMUT_P3, + JPEGR_COLORGAMUT_BT2100, +} jpegr_color_gamut; + +// Transfer functions for image data +typedef enum { + JPEGR_TF_UNSPECIFIED = -1, + JPEGR_TF_LINEAR = 0, + JPEGR_TF_HLG = 1, + JPEGR_TF_PQ = 2, + JPEGR_TF_SRGB = 3, +} jpegr_transfer_function; + +struct jpegr_info_struct { + size_t width; + size_t height; + std::vector* iccData; + std::vector* exifData; +}; + +/* + * Holds information for uncompressed image or recovery map. + */ +struct jpegr_uncompressed_struct { + // Pointer to the data location. + void* data; + // Width of the recovery map or image in pixels. + int width; + // Height of the recovery map or image in pixels. + int height; + // Color gamut. + jpegr_color_gamut colorGamut; +}; + +/* + * Holds information for compressed image or recovery map. + */ +struct jpegr_compressed_struct { + // Pointer to the data location. + void* data; + // Used data length in bytes. + int length; + // Maximum available data length in bytes. + int maxLength; + // Color gamut. + jpegr_color_gamut colorGamut; +}; + +/* + * Holds information for EXIF metadata. + */ +struct jpegr_exif_struct { + // Pointer to the data location. + void* data; + // Data length; + int length; +}; + +struct jpegr_metadata { + // JPEG/R version + uint32_t version; + // Max Content Boost for the map + float maxContentBoost; + // Min Content Boost for the map + float minContentBoost; +}; + +typedef struct jpegr_uncompressed_struct* jr_uncompressed_ptr; +typedef struct jpegr_compressed_struct* jr_compressed_ptr; +typedef struct jpegr_exif_struct* jr_exif_ptr; +typedef struct jpegr_metadata* jr_metadata_ptr; +typedef struct jpegr_info_struct* jr_info_ptr; + +class JpegR { +public: + /* + * Encode API-0 + * Compress JPEGR image from 10-bit HDR YUV. + * + * Tonemap the HDR input to a SDR image, generate recovery map from the HDR and SDR images, + * compress SDR YUV to 8-bit JPEG and append the recovery map to the end of the compressed + * JPEG. + * @param uncompressed_p010_image uncompressed HDR image in P010 color format + * @param hdr_tf transfer function of the HDR image + * @param dest destination of the compressed JPEGR image + * @param quality target quality of the JPEG encoding, must be in range of 0-100 where 100 is + * the highest quality + * @param exif pointer to the exif metadata. + * @return NO_ERROR if encoding succeeds, error code if error occurs. + */ + status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest, + int quality, + jr_exif_ptr exif); + + /* + * Encode API-1 + * Compress JPEGR image from 10-bit HDR YUV and 8-bit SDR YUV. + * + * Generate recovery map from the HDR and SDR inputs, compress SDR YUV to 8-bit JPEG and append + * the recovery map to the end of the compressed JPEG. HDR and SDR inputs must be the same + * resolution. + * @param uncompressed_p010_image uncompressed HDR image in P010 color format + * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format + * @param hdr_tf transfer function of the HDR image + * @param dest destination of the compressed JPEGR image + * @param quality target quality of the JPEG encoding, must be in range of 0-100 where 100 is + * the highest quality + * @param exif pointer to the exif metadata. + * @return NO_ERROR if encoding succeeds, error code if error occurs. + */ + status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_uncompressed_ptr uncompressed_yuv_420_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest, + int quality, + jr_exif_ptr exif); + + /* + * Encode API-2 + * Compress JPEGR image from 10-bit HDR YUV, 8-bit SDR YUV and compressed 8-bit JPEG. + * + * This method requires HAL Hardware JPEG encoder. + * + * Generate recovery map from the HDR and SDR inputs, append the recovery map to the end of the + * compressed JPEG. HDR and SDR inputs must be the same resolution and color space. + * @param uncompressed_p010_image uncompressed HDR image in P010 color format + * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format + * Note: the SDR image must be the decoded version of the JPEG + * input + * @param compressed_jpeg_image compressed 8-bit JPEG image + * @param hdr_tf transfer function of the HDR image + * @param dest destination of the compressed JPEGR image + * @return NO_ERROR if encoding succeeds, error code if error occurs. + */ + status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_compressed_ptr compressed_jpeg_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest); + + /* + * Encode API-3 + * Compress JPEGR image from 10-bit HDR YUV and 8-bit SDR YUV. + * + * This method requires HAL Hardware JPEG encoder. + * + * Decode the compressed 8-bit JPEG image to YUV SDR, generate recovery map from the HDR input + * and the decoded SDR result, append the recovery map to the end of the compressed JPEG. HDR + * and SDR inputs must be the same resolution. + * @param uncompressed_p010_image uncompressed HDR image in P010 color format + * @param compressed_jpeg_image compressed 8-bit JPEG image + * @param hdr_tf transfer function of the HDR image + * @param dest destination of the compressed JPEGR image + * @return NO_ERROR if encoding succeeds, error code if error occurs. + */ + status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_compressed_ptr compressed_jpeg_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest); + + /* + * Decode API + * Decompress JPEGR image. + * + * The output JPEGR image is in RGBA_1010102 data format if decoding to HDR. + * @param compressed_jpegr_image compressed JPEGR image + * @param dest destination of the uncompressed JPEGR image + * @param exif destination of the decoded EXIF metadata. + * @param request_sdr flag that request SDR output. If set to true, decoder will only decode + * the primary image which is SDR. Setting of request_sdr and input source + * (HDR or SDR) can be found in the table below: + * | input source | request_sdr | output of decoding | + * | HDR | true | SDR | + * | HDR | false | HDR | + * | SDR | true | SDR | + * | SDR | false | SDR | + * @return NO_ERROR if decoding succeeds, error code if error occurs. + */ + status_t decodeJPEGR(jr_compressed_ptr compressed_jpegr_image, + jr_uncompressed_ptr dest, + jr_exif_ptr exif = nullptr, + bool request_sdr = false); + + /* + * Gets Info from JPEGR file without decoding it. + * + * The output is filled jpegr_info structure + * @param compressed_jpegr_image compressed JPEGR image + * @param jpegr_info pointer to output JPEGR info. Members of jpegr_info + * are owned by the caller + * @return NO_ERROR if JPEGR parsing succeeds, error code otherwise + */ + status_t getJPEGRInfo(jr_compressed_ptr compressed_jpegr_image, + jr_info_ptr jpegr_info); +protected: + /* + * This method is called in the encoding pipeline. It will take the uncompressed 8-bit and + * 10-bit yuv images as input, and calculate the uncompressed recovery map. The input images + * must be the same resolution. + * + * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format + * @param uncompressed_p010_image uncompressed HDR image in P010 color format + * @param hdr_tf transfer function of the HDR image + * @param dest recovery map; caller responsible for memory of data + * @param metadata max_content_boost is filled in + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_uncompressed_ptr uncompressed_p010_image, + jpegr_transfer_function hdr_tf, + jr_metadata_ptr metadata, + jr_uncompressed_ptr dest); + + /* + * This method is called in the decoding pipeline. It will take the uncompressed (decoded) + * 8-bit yuv image, the uncompressed (decoded) recovery map, and extracted JPEG/R metadata as + * input, and calculate the 10-bit recovered image. The recovered output image is the same + * color gamut as the SDR image, with HLG transfer function, and is in RGBA1010102 data format. + * + * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format + * @param uncompressed_recovery_map uncompressed recovery map + * @param metadata JPEG/R metadata extracted from XMP. + * @param dest reconstructed HDR image + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t applyRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_uncompressed_ptr uncompressed_recovery_map, + jr_metadata_ptr metadata, + jr_uncompressed_ptr dest); + +private: + /* + * This method is called in the encoding pipeline. It will encode the recovery map. + * + * @param uncompressed_recovery_map uncompressed recovery map + * @param dest encoded recover map + * @return NO_ERROR if encoding succeeds, error code if error occurs. + */ + status_t compressRecoveryMap(jr_uncompressed_ptr uncompressed_recovery_map, + jr_compressed_ptr dest); + + /* + * This methoud is called to separate primary image and recovery map image from JPEGR + * + * @param compressed_jpegr_image compressed JPEGR image + * @param primary_image destination of primary image + * @param recovery_map destination of compressed recovery map + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t extractPrimaryImageAndRecoveryMap(jr_compressed_ptr compressed_jpegr_image, + jr_compressed_ptr primary_image, + jr_compressed_ptr recovery_map); + /* + * This method is called in the decoding pipeline. It will read XMP metadata to find the start + * position of the compressed recovery map, and will extract the compressed recovery map. + * + * @param compressed_jpegr_image compressed JPEGR image + * @param dest destination of compressed recovery map + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t extractRecoveryMap(jr_compressed_ptr compressed_jpegr_image, + jr_compressed_ptr dest); + + /* + * This method is called in the encoding pipeline. It will take the standard 8-bit JPEG image, + * the compressed recovery map and optionally the exif package as inputs, and generate the XMP + * metadata, and finally append everything in the order of: + * SOI, APP2(EXIF) (if EXIF is from outside), APP2(XMP), primary image, recovery map + * Note that EXIF package is only available for encoding API-0 and API-1. For encoding API-2 and + * API-3 this parameter is null, but the primary image in JPEG/R may still have EXIF as long as + * the input JPEG has EXIF. + * + * @param compressed_jpeg_image compressed 8-bit JPEG image + * @param compress_recovery_map compressed recover map + * @param (nullable) exif EXIF package + * @param metadata JPEG/R metadata to encode in XMP of the jpeg + * @param dest compressed JPEGR image + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image, + jr_compressed_ptr compressed_recovery_map, + jr_exif_ptr exif, + jr_metadata_ptr metadata, + jr_compressed_ptr dest); + + /* + * This method will tone map a HDR image to an SDR image. + * + * @param src (input) uncompressed P010 image + * @param dest (output) tone mapping result as a YUV_420 image + * @return NO_ERROR if calculation succeeds, error code if error occurs. + */ + status_t toneMap(jr_uncompressed_ptr src, + jr_uncompressed_ptr dest); +}; + +} // namespace android::recoverymap + +#endif // ANDROID_JPEGRECOVERYMAP_JPEGR_H diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/jpegrutils.h b/libs/jpegrecoverymap/include/jpegrecoverymap/jpegrutils.h new file mode 100644 index 0000000000..73867f9552 --- /dev/null +++ b/libs/jpegrecoverymap/include/jpegrecoverymap/jpegrutils.h @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_JPEGRECOVERYMAP_JPEGRUTILS_H +#define ANDROID_JPEGRECOVERYMAP_JPEGRUTILS_H + +#include + +#include +#include +#include +#include + +namespace android::recoverymap { + +struct jpegr_metadata; + +/* + * Helper function used for writing data to destination. + * + * @param destination destination of the data to be written. + * @param source source of data being written. + * @param length length of the data to be written. + * @param position cursor in desitination where the data is to be written. + * @return status of succeed or error code. + */ +status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int &position); + + +/* + * Parses XMP packet and fills metadata with data from XMP + * + * @param xmp_data pointer to XMP packet + * @param xmp_size size of XMP packet + * @param metadata place to store HDR metadata values + * @return true if metadata is successfully retrieved, false otherwise +*/ +bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, jpegr_metadata* metadata); + +/* + * This method generates XMP metadata. + * + * below is an example of the XMP metadata that this function generates where + * secondary_image_length = 1000 + * max_content_boost = 8.0 + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param secondary_image_length length of secondary image + * @param metadata JPEG/R metadata to encode as XMP + * @return XMP metadata in type of string + */ +std::string generateXmp(int secondary_image_length, jpegr_metadata& metadata); +} + +#endif //ANDROID_JPEGRECOVERYMAP_JPEGRUTILS_H diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h deleted file mode 100644 index 1fd129b991..0000000000 --- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_JPEGRECOVERYMAP_RECOVERYMAP_H -#define ANDROID_JPEGRECOVERYMAP_RECOVERYMAP_H - -#include "jpegrerrorcode.h" - -namespace android::recoverymap { - -// Color gamuts for image data -typedef enum { - JPEGR_COLORGAMUT_UNSPECIFIED, - JPEGR_COLORGAMUT_BT709, - JPEGR_COLORGAMUT_P3, - JPEGR_COLORGAMUT_BT2100, -} jpegr_color_gamut; - -// Transfer functions for image data -typedef enum { - JPEGR_TF_UNSPECIFIED = -1, - JPEGR_TF_LINEAR = 0, - JPEGR_TF_HLG = 1, - JPEGR_TF_PQ = 2, - JPEGR_TF_SRGB = 3, -} jpegr_transfer_function; - -struct jpegr_info_struct { - size_t width; - size_t height; - std::vector* iccData; - std::vector* exifData; -}; - -/* - * Holds information for uncompressed image or recovery map. - */ -struct jpegr_uncompressed_struct { - // Pointer to the data location. - void* data; - // Width of the recovery map or image in pixels. - int width; - // Height of the recovery map or image in pixels. - int height; - // Color gamut. - jpegr_color_gamut colorGamut; -}; - -/* - * Holds information for compressed image or recovery map. - */ -struct jpegr_compressed_struct { - // Pointer to the data location. - void* data; - // Used data length in bytes. - int length; - // Maximum available data length in bytes. - int maxLength; - // Color gamut. - jpegr_color_gamut colorGamut; -}; - -/* - * Holds information for EXIF metadata. - */ -struct jpegr_exif_struct { - // Pointer to the data location. - void* data; - // Data length; - int length; -}; - -struct jpegr_metadata { - // JPEG/R version - uint32_t version; - // Max Content Boost for the map - float maxContentBoost; - // Min Content Boost for the map - float minContentBoost; -}; - -typedef struct jpegr_uncompressed_struct* jr_uncompressed_ptr; -typedef struct jpegr_compressed_struct* jr_compressed_ptr; -typedef struct jpegr_exif_struct* jr_exif_ptr; -typedef struct jpegr_metadata* jr_metadata_ptr; -typedef struct jpegr_info_struct* jr_info_ptr; - -class RecoveryMap { -public: - /* - * Encode API-0 - * Compress JPEGR image from 10-bit HDR YUV. - * - * Tonemap the HDR input to a SDR image, generate recovery map from the HDR and SDR images, - * compress SDR YUV to 8-bit JPEG and append the recovery map to the end of the compressed - * JPEG. - * @param uncompressed_p010_image uncompressed HDR image in P010 color format - * @param hdr_tf transfer function of the HDR image - * @param dest destination of the compressed JPEGR image - * @param quality target quality of the JPEG encoding, must be in range of 0-100 where 100 is - * the highest quality - * @param exif pointer to the exif metadata. - * @return NO_ERROR if encoding succeeds, error code if error occurs. - */ - status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest, - int quality, - jr_exif_ptr exif); - - /* - * Encode API-1 - * Compress JPEGR image from 10-bit HDR YUV and 8-bit SDR YUV. - * - * Generate recovery map from the HDR and SDR inputs, compress SDR YUV to 8-bit JPEG and append - * the recovery map to the end of the compressed JPEG. HDR and SDR inputs must be the same - * resolution. - * @param uncompressed_p010_image uncompressed HDR image in P010 color format - * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format - * @param hdr_tf transfer function of the HDR image - * @param dest destination of the compressed JPEGR image - * @param quality target quality of the JPEG encoding, must be in range of 0-100 where 100 is - * the highest quality - * @param exif pointer to the exif metadata. - * @return NO_ERROR if encoding succeeds, error code if error occurs. - */ - status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_uncompressed_ptr uncompressed_yuv_420_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest, - int quality, - jr_exif_ptr exif); - - /* - * Encode API-2 - * Compress JPEGR image from 10-bit HDR YUV, 8-bit SDR YUV and compressed 8-bit JPEG. - * - * This method requires HAL Hardware JPEG encoder. - * - * Generate recovery map from the HDR and SDR inputs, append the recovery map to the end of the - * compressed JPEG. HDR and SDR inputs must be the same resolution and color space. - * @param uncompressed_p010_image uncompressed HDR image in P010 color format - * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format - * Note: the SDR image must be the decoded version of the JPEG - * input - * @param compressed_jpeg_image compressed 8-bit JPEG image - * @param hdr_tf transfer function of the HDR image - * @param dest destination of the compressed JPEGR image - * @return NO_ERROR if encoding succeeds, error code if error occurs. - */ - status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_compressed_ptr compressed_jpeg_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest); - - /* - * Encode API-3 - * Compress JPEGR image from 10-bit HDR YUV and 8-bit SDR YUV. - * - * This method requires HAL Hardware JPEG encoder. - * - * Decode the compressed 8-bit JPEG image to YUV SDR, generate recovery map from the HDR input - * and the decoded SDR result, append the recovery map to the end of the compressed JPEG. HDR - * and SDR inputs must be the same resolution. - * @param uncompressed_p010_image uncompressed HDR image in P010 color format - * @param compressed_jpeg_image compressed 8-bit JPEG image - * @param hdr_tf transfer function of the HDR image - * @param dest destination of the compressed JPEGR image - * @return NO_ERROR if encoding succeeds, error code if error occurs. - */ - status_t encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_compressed_ptr compressed_jpeg_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest); - - /* - * Decode API - * Decompress JPEGR image. - * - * The output JPEGR image is in RGBA_1010102 data format if decoding to HDR. - * @param compressed_jpegr_image compressed JPEGR image - * @param dest destination of the uncompressed JPEGR image - * @param exif destination of the decoded EXIF metadata. - * @param request_sdr flag that request SDR output. If set to true, decoder will only decode - * the primary image which is SDR. Setting of request_sdr and input source - * (HDR or SDR) can be found in the table below: - * | input source | request_sdr | output of decoding | - * | HDR | true | SDR | - * | HDR | false | HDR | - * | SDR | true | SDR | - * | SDR | false | SDR | - * @return NO_ERROR if decoding succeeds, error code if error occurs. - */ - status_t decodeJPEGR(jr_compressed_ptr compressed_jpegr_image, - jr_uncompressed_ptr dest, - jr_exif_ptr exif = nullptr, - bool request_sdr = false); - - /* - * Gets Info from JPEGR file without decoding it. - * - * The output is filled jpegr_info structure - * @param compressed_jpegr_image compressed JPEGR image - * @param jpegr_info pointer to output JPEGR info. Members of jpegr_info - * are owned by the caller - * @return NO_ERROR if JPEGR parsing succeeds, error code otherwise - */ - status_t getJPEGRInfo(jr_compressed_ptr compressed_jpegr_image, - jr_info_ptr jpegr_info); - -protected: - // Following functions protected instead of private for testing. - - /* - * This method is called in the encoding pipeline. It will take the uncompressed 8-bit and - * 10-bit yuv images as input, and calculate the uncompressed recovery map. The input images - * must be the same resolution. - * - * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format - * @param uncompressed_p010_image uncompressed HDR image in P010 color format - * @param hdr_tf transfer function of the HDR image - * @param dest recovery map; caller responsible for memory of data - * @param metadata minContentBoost and maxContentBoost are filled in - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_uncompressed_ptr uncompressed_p010_image, - jpegr_transfer_function hdr_tf, - jr_metadata_ptr metadata, - jr_uncompressed_ptr dest); - - /* - * This method is called in the decoding pipeline. It will take the uncompressed (decoded) - * 8-bit yuv image, the uncompressed (decoded) recovery map, and extracted JPEG/R metadata as - * input, and calculate the 10-bit recovered image. The recovered output image is the same - * color gamut as the SDR image, with HLG transfer function, and is in RGBA1010102 data format. - * - * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format - * @param uncompressed_recovery_map uncompressed recovery map - * @param metadata JPEG/R metadata extracted from XMP. - * @param dest reconstructed HDR image - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t applyRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_uncompressed_ptr uncompressed_recovery_map, - jr_metadata_ptr metadata, - jr_uncompressed_ptr dest); - -private: - /* - * This method is called in the encoding pipeline. It will encode the recovery map. - * - * @param uncompressed_recovery_map uncompressed recovery map - * @param dest encoded recover map - * @return NO_ERROR if encoding succeeds, error code if error occurs. - */ - status_t compressRecoveryMap(jr_uncompressed_ptr uncompressed_recovery_map, - jr_compressed_ptr dest); - - /* - * This methoud is called to separate primary image and recovery map image from JPEGR - * - * @param compressed_jpegr_image compressed JPEGR image - * @param primary_image destination of primary image - * @param recovery_map destination of compressed recovery map - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t extractPrimaryImageAndRecoveryMap(jr_compressed_ptr compressed_jpegr_image, - jr_compressed_ptr primary_image, - jr_compressed_ptr recovery_map); - /* - * This method is called in the decoding pipeline. It will read XMP metadata to find the start - * position of the compressed recovery map, and will extract the compressed recovery map. - * - * @param compressed_jpegr_image compressed JPEGR image - * @param dest destination of compressed recovery map - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t extractRecoveryMap(jr_compressed_ptr compressed_jpegr_image, - jr_compressed_ptr dest); - - /* - * This method is called in the encoding pipeline. It will take the standard 8-bit JPEG image, - * the compressed recovery map and optionally the exif package as inputs, and generate the XMP - * metadata, and finally append everything in the order of: - * SOI, APP2(EXIF) (if EXIF is from outside), APP2(XMP), primary image, recovery map - * Note that EXIF package is only available for encoding API-0 and API-1. For encoding API-2 and - * API-3 this parameter is null, but the primary image in JPEG/R may still have EXIF as long as - * the input JPEG has EXIF. - * - * @param compressed_jpeg_image compressed 8-bit JPEG image - * @param compress_recovery_map compressed recover map - * @param (nullable) exif EXIF package - * @param metadata JPEG/R metadata to encode in XMP of the jpeg - * @param dest compressed JPEGR image - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image, - jr_compressed_ptr compressed_recovery_map, - jr_exif_ptr exif, - jr_metadata_ptr metadata, - jr_compressed_ptr dest); - - /* - * This method will tone map a HDR image to an SDR image. - * - * @param src (input) uncompressed P010 image - * @param dest (output) tone mapping result as a YUV_420 image - * @return NO_ERROR if calculation succeeds, error code if error occurs. - */ - status_t toneMap(jr_uncompressed_ptr src, - jr_uncompressed_ptr dest); -}; - -} // namespace android::recoverymap - -#endif // ANDROID_JPEGRECOVERYMAP_RECOVERYMAP_H diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h index 6eed08afb4..71b18b9094 100644 --- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h +++ b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymapmath.h @@ -20,7 +20,7 @@ #include #include -#include +#include namespace android::recoverymap { diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h deleted file mode 100644 index de29a339ed..0000000000 --- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_JPEGRECOVERYMAP_RECOVERYMAPUTILS_H -#define ANDROID_JPEGRECOVERYMAP_RECOVERYMAPUTILS_H - -#include - -#include -#include -#include -#include - -namespace android::recoverymap { - -struct jpegr_metadata; - -/* - * Helper function used for writing data to destination. - * - * @param destination destination of the data to be written. - * @param source source of data being written. - * @param length length of the data to be written. - * @param position cursor in desitination where the data is to be written. - * @return status of succeed or error code. - */ -status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int &position); - - -/* - * Parses XMP packet and fills metadata with data from XMP - * - * @param xmp_data pointer to XMP packet - * @param xmp_size size of XMP packet - * @param metadata place to store HDR metadata values - * @return true if metadata is successfully retrieved, false otherwise -*/ -bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, jpegr_metadata* metadata); - -/* - * This method generates XMP metadata. - * - * below is an example of the XMP metadata that this function generates where - * secondary_image_length = 1000 - * max_content_boost = 8.0 - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * @param secondary_image_length length of secondary image - * @param metadata JPEG/R metadata to encode as XMP - * @return XMP metadata in type of string - */ -std::string generateXmp(int secondary_image_length, jpegr_metadata& metadata); -} - -#endif //ANDROID_JPEGRECOVERYMAP_RECOVERYMAPUTILS_H diff --git a/libs/jpegrecoverymap/jpegr.cpp b/libs/jpegrecoverymap/jpegr.cpp new file mode 100644 index 0000000000..fbd511a600 --- /dev/null +++ b/libs/jpegrecoverymap/jpegr.cpp @@ -0,0 +1,957 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "SkColorSpace.h" +#include "SkICC.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace photos_editing_formats::image_io; + +namespace android::recoverymap { + +#define USE_SRGB_INVOETF_LUT 1 +#define USE_HLG_OETF_LUT 1 +#define USE_PQ_OETF_LUT 1 +#define USE_HLG_INVOETF_LUT 1 +#define USE_PQ_INVOETF_LUT 1 +#define USE_APPLY_RECOVERY_LUT 1 + +#define JPEGR_CHECK(x) \ + { \ + status_t status = (x); \ + if ((status) != NO_ERROR) { \ + return status; \ + } \ + } + +// The current JPEGR version that we encode to +static const uint32_t kJpegrVersion = 1; + +// Map is quarter res / sixteenth size +static const size_t kMapDimensionScaleFactor = 4; +// JPEG block size. +// JPEG encoding / decoding will require 8 x 8 DCT transform. +// Width must be 8 dividable, and height must be 2 dividable. +static const size_t kJpegBlock = 8; +// JPEG compress quality (0 ~ 100) for recovery map +static const int kMapCompressQuality = 85; + +#define CONFIG_MULTITHREAD 1 +int GetCPUCoreCount() { + int cpuCoreCount = 1; +#if CONFIG_MULTITHREAD +#if defined(_SC_NPROCESSORS_ONLN) + cpuCoreCount = sysconf(_SC_NPROCESSORS_ONLN); +#else + // _SC_NPROC_ONLN must be defined... + cpuCoreCount = sysconf(_SC_NPROC_ONLN); +#endif +#endif + return cpuCoreCount; +} + +static const map jrGamut_to_skGamut { + {JPEGR_COLORGAMUT_BT709, SkNamedGamut::kSRGB}, + {JPEGR_COLORGAMUT_P3, SkNamedGamut::kDisplayP3}, + {JPEGR_COLORGAMUT_BT2100, SkNamedGamut::kRec2020}, +}; + +static const map< + recoverymap::jpegr_transfer_function, skcms_TransferFunction> jrTransFunc_to_skTransFunc { + {JPEGR_TF_SRGB, SkNamedTransferFn::kSRGB}, + {JPEGR_TF_LINEAR, SkNamedTransferFn::kLinear}, + {JPEGR_TF_HLG, SkNamedTransferFn::kHLG}, + {JPEGR_TF_PQ, SkNamedTransferFn::kPQ}, +}; + +/* Encode API-0 */ +status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest, + int quality, + jr_exif_ptr exif) { + if (uncompressed_p010_image == nullptr || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + if (quality < 0 || quality > 100) { + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + if (uncompressed_p010_image->width % kJpegBlock != 0 + || uncompressed_p010_image->height % 2 != 0) { + ALOGE("Image size can not be handled: %dx%d", + uncompressed_p010_image->width, uncompressed_p010_image->height); + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + jpegr_metadata metadata; + metadata.version = kJpegrVersion; + + jpegr_uncompressed_struct uncompressed_yuv_420_image; + unique_ptr uncompressed_yuv_420_image_data = make_unique( + uncompressed_p010_image->width * uncompressed_p010_image->height * 3 / 2); + uncompressed_yuv_420_image.data = uncompressed_yuv_420_image_data.get(); + JPEGR_CHECK(toneMap(uncompressed_p010_image, &uncompressed_yuv_420_image)); + + jpegr_uncompressed_struct map; + JPEGR_CHECK(generateRecoveryMap( + &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); + std::unique_ptr map_data; + map_data.reset(reinterpret_cast(map.data)); + + jpegr_compressed_struct compressed_map; + compressed_map.maxLength = map.width * map.height; + unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); + compressed_map.data = compressed_map_data.get(); + JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); + + sk_sp icc = SkWriteICCProfile( + jrTransFunc_to_skTransFunc.at(JPEGR_TF_SRGB), + jrGamut_to_skGamut.at(uncompressed_yuv_420_image.colorGamut)); + + JpegEncoderHelper jpeg_encoder; + if (!jpeg_encoder.compressImage(uncompressed_yuv_420_image.data, + uncompressed_yuv_420_image.width, + uncompressed_yuv_420_image.height, quality, + icc.get()->data(), icc.get()->size())) { + return ERROR_JPEGR_ENCODE_ERROR; + } + jpegr_compressed_struct jpeg; + jpeg.data = jpeg_encoder.getCompressedImagePtr(); + jpeg.length = jpeg_encoder.getCompressedImageSize(); + + JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, exif, &metadata, dest)); + + return NO_ERROR; +} + +/* Encode API-1 */ +status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_uncompressed_ptr uncompressed_yuv_420_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest, + int quality, + jr_exif_ptr exif) { + if (uncompressed_p010_image == nullptr + || uncompressed_yuv_420_image == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + if (quality < 0 || quality > 100) { + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + if (uncompressed_p010_image->width != uncompressed_yuv_420_image->width + || uncompressed_p010_image->height != uncompressed_yuv_420_image->height) { + return ERROR_JPEGR_RESOLUTION_MISMATCH; + } + + if (uncompressed_p010_image->width % kJpegBlock != 0 + || uncompressed_p010_image->height % 2 != 0) { + ALOGE("Image size can not be handled: %dx%d", + uncompressed_p010_image->width, uncompressed_p010_image->height); + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + jpegr_metadata metadata; + metadata.version = kJpegrVersion; + + jpegr_uncompressed_struct map; + JPEGR_CHECK(generateRecoveryMap( + uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); + std::unique_ptr map_data; + map_data.reset(reinterpret_cast(map.data)); + + jpegr_compressed_struct compressed_map; + compressed_map.maxLength = map.width * map.height; + unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); + compressed_map.data = compressed_map_data.get(); + JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); + + sk_sp icc = SkWriteICCProfile( + jrTransFunc_to_skTransFunc.at(JPEGR_TF_SRGB), + jrGamut_to_skGamut.at(uncompressed_yuv_420_image->colorGamut)); + + JpegEncoderHelper jpeg_encoder; + if (!jpeg_encoder.compressImage(uncompressed_yuv_420_image->data, + uncompressed_yuv_420_image->width, + uncompressed_yuv_420_image->height, quality, + icc.get()->data(), icc.get()->size())) { + return ERROR_JPEGR_ENCODE_ERROR; + } + jpegr_compressed_struct jpeg; + jpeg.data = jpeg_encoder.getCompressedImagePtr(); + jpeg.length = jpeg_encoder.getCompressedImageSize(); + + JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, exif, &metadata, dest)); + + return NO_ERROR; +} + +/* Encode API-2 */ +status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_compressed_ptr compressed_jpeg_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest) { + if (uncompressed_p010_image == nullptr + || uncompressed_yuv_420_image == nullptr + || compressed_jpeg_image == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + if (uncompressed_p010_image->width != uncompressed_yuv_420_image->width + || uncompressed_p010_image->height != uncompressed_yuv_420_image->height) { + return ERROR_JPEGR_RESOLUTION_MISMATCH; + } + + if (uncompressed_p010_image->width % kJpegBlock != 0 + || uncompressed_p010_image->height % 2 != 0) { + ALOGE("Image size can not be handled: %dx%d", + uncompressed_p010_image->width, uncompressed_p010_image->height); + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + jpegr_metadata metadata; + metadata.version = kJpegrVersion; + + jpegr_uncompressed_struct map; + JPEGR_CHECK(generateRecoveryMap( + uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); + std::unique_ptr map_data; + map_data.reset(reinterpret_cast(map.data)); + + jpegr_compressed_struct compressed_map; + compressed_map.maxLength = map.width * map.height; + unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); + compressed_map.data = compressed_map_data.get(); + JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); + + JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); + + return NO_ERROR; +} + +/* Encode API-3 */ +status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, + jr_compressed_ptr compressed_jpeg_image, + jpegr_transfer_function hdr_tf, + jr_compressed_ptr dest) { + if (uncompressed_p010_image == nullptr + || compressed_jpeg_image == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + if (uncompressed_p010_image->width % kJpegBlock != 0 + || uncompressed_p010_image->height % 2 != 0) { + ALOGE("Image size can not be handled: %dx%d", + uncompressed_p010_image->width, uncompressed_p010_image->height); + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + JpegDecoderHelper jpeg_decoder; + if (!jpeg_decoder.decompressImage(compressed_jpeg_image->data, compressed_jpeg_image->length)) { + return ERROR_JPEGR_DECODE_ERROR; + } + jpegr_uncompressed_struct uncompressed_yuv_420_image; + uncompressed_yuv_420_image.data = jpeg_decoder.getDecompressedImagePtr(); + uncompressed_yuv_420_image.width = jpeg_decoder.getDecompressedImageWidth(); + uncompressed_yuv_420_image.height = jpeg_decoder.getDecompressedImageHeight(); + uncompressed_yuv_420_image.colorGamut = compressed_jpeg_image->colorGamut; + + if (uncompressed_p010_image->width != uncompressed_yuv_420_image.width + || uncompressed_p010_image->height != uncompressed_yuv_420_image.height) { + return ERROR_JPEGR_RESOLUTION_MISMATCH; + } + + jpegr_metadata metadata; + metadata.version = kJpegrVersion; + + jpegr_uncompressed_struct map; + JPEGR_CHECK(generateRecoveryMap( + &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); + std::unique_ptr map_data; + map_data.reset(reinterpret_cast(map.data)); + + jpegr_compressed_struct compressed_map; + compressed_map.maxLength = map.width * map.height; + unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); + compressed_map.data = compressed_map_data.get(); + JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); + + JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); + + return NO_ERROR; +} + +status_t JpegR::getJPEGRInfo(jr_compressed_ptr compressed_jpegr_image, + jr_info_ptr jpegr_info) { + if (compressed_jpegr_image == nullptr || jpegr_info == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + jpegr_compressed_struct primary_image, recovery_map; + JPEGR_CHECK(extractPrimaryImageAndRecoveryMap(compressed_jpegr_image, + &primary_image, &recovery_map)); + + JpegDecoderHelper jpeg_decoder; + if (!jpeg_decoder.getCompressedImageParameters(primary_image.data, primary_image.length, + &jpegr_info->width, &jpegr_info->height, + jpegr_info->iccData, jpegr_info->exifData)) { + return ERROR_JPEGR_DECODE_ERROR; + } + + return NO_ERROR; +} + +/* Decode API */ +status_t JpegR::decodeJPEGR(jr_compressed_ptr compressed_jpegr_image, + jr_uncompressed_ptr dest, + jr_exif_ptr exif, + bool request_sdr) { + if (compressed_jpegr_image == nullptr || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + // TODO: fill EXIF data + (void) exif; + + if (request_sdr) { + JpegDecoderHelper jpeg_decoder; + if (!jpeg_decoder.decompressImage(compressed_jpegr_image->data, compressed_jpegr_image->length, + true)) { + return ERROR_JPEGR_DECODE_ERROR; + } + jpegr_uncompressed_struct uncompressed_rgba_image; + uncompressed_rgba_image.data = jpeg_decoder.getDecompressedImagePtr(); + uncompressed_rgba_image.width = jpeg_decoder.getDecompressedImageWidth(); + uncompressed_rgba_image.height = jpeg_decoder.getDecompressedImageHeight(); + memcpy(dest->data, uncompressed_rgba_image.data, + uncompressed_rgba_image.width * uncompressed_rgba_image.height * 4); + dest->width = uncompressed_rgba_image.width; + dest->height = uncompressed_rgba_image.height; + return NO_ERROR; + } + + jpegr_compressed_struct compressed_map; + jpegr_metadata metadata; + JPEGR_CHECK(extractRecoveryMap(compressed_jpegr_image, &compressed_map)); + + JpegDecoderHelper jpeg_decoder; + if (!jpeg_decoder.decompressImage(compressed_jpegr_image->data, compressed_jpegr_image->length)) { + return ERROR_JPEGR_DECODE_ERROR; + } + + JpegDecoderHelper recovery_map_decoder; + if (!recovery_map_decoder.decompressImage(compressed_map.data, compressed_map.length)) { + return ERROR_JPEGR_DECODE_ERROR; + } + + jpegr_uncompressed_struct map; + map.data = recovery_map_decoder.getDecompressedImagePtr(); + map.width = recovery_map_decoder.getDecompressedImageWidth(); + map.height = recovery_map_decoder.getDecompressedImageHeight(); + + jpegr_uncompressed_struct uncompressed_yuv_420_image; + uncompressed_yuv_420_image.data = jpeg_decoder.getDecompressedImagePtr(); + uncompressed_yuv_420_image.width = jpeg_decoder.getDecompressedImageWidth(); + uncompressed_yuv_420_image.height = jpeg_decoder.getDecompressedImageHeight(); + + if (!getMetadataFromXMP(static_cast(jpeg_decoder.getXMPPtr()), + jpeg_decoder.getXMPSize(), &metadata)) { + return ERROR_JPEGR_DECODE_ERROR; + } + + JPEGR_CHECK(applyRecoveryMap(&uncompressed_yuv_420_image, &map, &metadata, dest)); + return NO_ERROR; +} + +status_t JpegR::compressRecoveryMap(jr_uncompressed_ptr uncompressed_recovery_map, + jr_compressed_ptr dest) { + if (uncompressed_recovery_map == nullptr || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + JpegEncoderHelper jpeg_encoder; + if (!jpeg_encoder.compressImage(uncompressed_recovery_map->data, + uncompressed_recovery_map->width, + uncompressed_recovery_map->height, + kMapCompressQuality, + nullptr, + 0, + true /* isSingleChannel */)) { + return ERROR_JPEGR_ENCODE_ERROR; + } + + if (dest->maxLength < jpeg_encoder.getCompressedImageSize()) { + return ERROR_JPEGR_BUFFER_TOO_SMALL; + } + + memcpy(dest->data, jpeg_encoder.getCompressedImagePtr(), jpeg_encoder.getCompressedImageSize()); + dest->length = jpeg_encoder.getCompressedImageSize(); + dest->colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED; + + return NO_ERROR; +} + +const int kJobSzInRows = 16; +static_assert(kJobSzInRows > 0 && kJobSzInRows % kMapDimensionScaleFactor == 0, + "align job size to kMapDimensionScaleFactor"); + +class JobQueue { + public: + bool dequeueJob(size_t& rowStart, size_t& rowEnd); + void enqueueJob(size_t rowStart, size_t rowEnd); + void markQueueForEnd(); + void reset(); + + private: + bool mQueuedAllJobs = false; + std::deque> mJobs; + std::mutex mMutex; + std::condition_variable mCv; +}; + +bool JobQueue::dequeueJob(size_t& rowStart, size_t& rowEnd) { + std::unique_lock lock{mMutex}; + while (true) { + if (mJobs.empty()) { + if (mQueuedAllJobs) { + return false; + } else { + mCv.wait(lock); + } + } else { + auto it = mJobs.begin(); + rowStart = std::get<0>(*it); + rowEnd = std::get<1>(*it); + mJobs.erase(it); + return true; + } + } + return false; +} + +void JobQueue::enqueueJob(size_t rowStart, size_t rowEnd) { + std::unique_lock lock{mMutex}; + mJobs.push_back(std::make_tuple(rowStart, rowEnd)); + lock.unlock(); + mCv.notify_one(); +} + +void JobQueue::markQueueForEnd() { + std::unique_lock lock{mMutex}; + mQueuedAllJobs = true; +} + +void JobQueue::reset() { + std::unique_lock lock{mMutex}; + mJobs.clear(); + mQueuedAllJobs = false; +} + +status_t JpegR::generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_uncompressed_ptr uncompressed_p010_image, + jpegr_transfer_function hdr_tf, + jr_metadata_ptr metadata, + jr_uncompressed_ptr dest) { + if (uncompressed_yuv_420_image == nullptr + || uncompressed_p010_image == nullptr + || metadata == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + if (uncompressed_yuv_420_image->width != uncompressed_p010_image->width + || uncompressed_yuv_420_image->height != uncompressed_p010_image->height) { + return ERROR_JPEGR_RESOLUTION_MISMATCH; + } + + if (uncompressed_yuv_420_image->colorGamut == JPEGR_COLORGAMUT_UNSPECIFIED + || uncompressed_p010_image->colorGamut == JPEGR_COLORGAMUT_UNSPECIFIED) { + return ERROR_JPEGR_INVALID_COLORGAMUT; + } + + size_t image_width = uncompressed_yuv_420_image->width; + size_t image_height = uncompressed_yuv_420_image->height; + size_t map_width = image_width / kMapDimensionScaleFactor; + size_t map_height = image_height / kMapDimensionScaleFactor; + size_t map_stride = static_cast( + floor((map_width + kJpegBlock - 1) / kJpegBlock)) * kJpegBlock; + size_t map_height_aligned = ((map_height + 1) >> 1) << 1; + + dest->width = map_stride; + dest->height = map_height_aligned; + dest->colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED; + dest->data = new uint8_t[map_stride * map_height_aligned]; + std::unique_ptr map_data; + map_data.reset(reinterpret_cast(dest->data)); + + ColorTransformFn hdrInvOetf = nullptr; + float hdr_white_nits = 0.0f; + switch (hdr_tf) { + case JPEGR_TF_LINEAR: + hdrInvOetf = identityConversion; + break; + case JPEGR_TF_HLG: +#if USE_HLG_INVOETF_LUT + hdrInvOetf = hlgInvOetfLUT; +#else + hdrInvOetf = hlgInvOetf; +#endif + hdr_white_nits = kHlgMaxNits; + break; + case JPEGR_TF_PQ: +#if USE_PQ_INVOETF_LUT + hdrInvOetf = pqInvOetfLUT; +#else + hdrInvOetf = pqInvOetf; +#endif + hdr_white_nits = kPqMaxNits; + break; + default: + // Should be impossible to hit after input validation. + return ERROR_JPEGR_INVALID_TRANS_FUNC; + } + + ColorTransformFn hdrGamutConversionFn = getHdrConversionFn( + uncompressed_yuv_420_image->colorGamut, uncompressed_p010_image->colorGamut); + + ColorCalculationFn luminanceFn = nullptr; + switch (uncompressed_yuv_420_image->colorGamut) { + case JPEGR_COLORGAMUT_BT709: + luminanceFn = srgbLuminance; + break; + case JPEGR_COLORGAMUT_P3: + luminanceFn = p3Luminance; + break; + case JPEGR_COLORGAMUT_BT2100: + luminanceFn = bt2100Luminance; + break; + case JPEGR_COLORGAMUT_UNSPECIFIED: + // Should be impossible to hit after input validation. + return ERROR_JPEGR_INVALID_COLORGAMUT; + } + + std::mutex mutex; + float max_gain = 0.0f; + float min_gain = 1.0f; + const int threads = std::clamp(GetCPUCoreCount(), 1, 4); + size_t rowStep = threads == 1 ? image_height : kJobSzInRows; + JobQueue jobQueue; + + std::function computeMetadata = [uncompressed_p010_image, uncompressed_yuv_420_image, + hdrInvOetf, hdrGamutConversionFn, luminanceFn, + hdr_white_nits, threads, &mutex, &max_gain, &min_gain, + &jobQueue]() -> void { + size_t rowStart, rowEnd; + float max_gain_th = 0.0f; + float min_gain_th = 1.0f; + + while (jobQueue.dequeueJob(rowStart, rowEnd)) { + for (size_t y = rowStart; y < rowEnd; ++y) { + for (size_t x = 0; x < uncompressed_p010_image->width; ++x) { + Color hdr_yuv_gamma = getP010Pixel(uncompressed_p010_image, x, y); + Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); + Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); + hdr_rgb = hdrGamutConversionFn(hdr_rgb); + float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits; + + Color sdr_yuv_gamma = + getYuv420Pixel(uncompressed_yuv_420_image, x, y); + Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma); +#if USE_SRGB_INVOETF_LUT + Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); +#else + Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); +#endif + float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; + + float gain = hdr_y_nits / sdr_y_nits; + max_gain_th = std::max(max_gain_th, gain); + min_gain_th = std::min(min_gain_th, gain); + } + } + } + std::unique_lock lock{mutex}; + max_gain = std::max(max_gain, max_gain_th); + min_gain = std::min(min_gain, min_gain_th); + }; + + std::function generateMap = [uncompressed_yuv_420_image, uncompressed_p010_image, + metadata, dest, hdrInvOetf, hdrGamutConversionFn, + luminanceFn, hdr_white_nits, &jobQueue]() -> void { + size_t rowStart, rowEnd; + size_t dest_map_width = uncompressed_yuv_420_image->width / kMapDimensionScaleFactor; + size_t dest_map_stride = dest->width; + while (jobQueue.dequeueJob(rowStart, rowEnd)) { + for (size_t y = rowStart; y < rowEnd; ++y) { + for (size_t x = 0; x < dest_map_width; ++x) { + Color sdr_yuv_gamma = + sampleYuv420(uncompressed_yuv_420_image, kMapDimensionScaleFactor, x, y); + Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma); +#if USE_SRGB_INVOETF_LUT + Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); +#else + Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); +#endif + float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; + + Color hdr_yuv_gamma = sampleP010(uncompressed_p010_image, kMapDimensionScaleFactor, x, y); + Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); + Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); + hdr_rgb = hdrGamutConversionFn(hdr_rgb); + float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits; + + size_t pixel_idx = x + y * dest_map_stride; + reinterpret_cast(dest->data)[pixel_idx] = + encodeRecovery(sdr_y_nits, hdr_y_nits, metadata); + } + } + } + }; + + std::vector workers; + for (int th = 0; th < threads - 1; th++) { + workers.push_back(std::thread(computeMetadata)); + } + + // compute metadata + for (size_t rowStart = 0; rowStart < image_height;) { + size_t rowEnd = std::min(rowStart + rowStep, image_height); + jobQueue.enqueueJob(rowStart, rowEnd); + rowStart = rowEnd; + } + jobQueue.markQueueForEnd(); + computeMetadata(); + std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); + workers.clear(); + + metadata->maxContentBoost = max_gain; + metadata->minContentBoost = min_gain; + + // generate map + jobQueue.reset(); + for (int th = 0; th < threads - 1; th++) { + workers.push_back(std::thread(generateMap)); + } + + rowStep = (threads == 1 ? image_height : kJobSzInRows) / kMapDimensionScaleFactor; + for (size_t rowStart = 0; rowStart < map_height;) { + size_t rowEnd = std::min(rowStart + rowStep, map_height); + jobQueue.enqueueJob(rowStart, rowEnd); + rowStart = rowEnd; + } + jobQueue.markQueueForEnd(); + generateMap(); + std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); + + map_data.release(); + return NO_ERROR; +} + +status_t JpegR::applyRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, + jr_uncompressed_ptr uncompressed_recovery_map, + jr_metadata_ptr metadata, + jr_uncompressed_ptr dest) { + if (uncompressed_yuv_420_image == nullptr + || uncompressed_recovery_map == nullptr + || metadata == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + dest->width = uncompressed_yuv_420_image->width; + dest->height = uncompressed_yuv_420_image->height; + ShepardsIDW idwTable(kMapDimensionScaleFactor); + RecoveryLUT recoveryLUT(metadata); + + JobQueue jobQueue; + std::function applyRecMap = [uncompressed_yuv_420_image, uncompressed_recovery_map, + metadata, dest, &jobQueue, &idwTable, + &recoveryLUT]() -> void { + const float hdr_ratio = metadata->maxContentBoost; + size_t width = uncompressed_yuv_420_image->width; + size_t height = uncompressed_yuv_420_image->height; + +#if USE_HLG_OETF_LUT + ColorTransformFn hdrOetf = hlgOetfLUT; +#else + ColorTransformFn hdrOetf = hlgOetf; +#endif + + size_t rowStart, rowEnd; + while (jobQueue.dequeueJob(rowStart, rowEnd)) { + for (size_t y = rowStart; y < rowEnd; ++y) { + for (size_t x = 0; x < width; ++x) { + Color yuv_gamma_sdr = getYuv420Pixel(uncompressed_yuv_420_image, x, y); + Color rgb_gamma_sdr = srgbYuvToRgb(yuv_gamma_sdr); +#if USE_SRGB_INVOETF_LUT + Color rgb_sdr = srgbInvOetfLUT(rgb_gamma_sdr); +#else + Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr); +#endif + float recovery; + // TODO: determine map scaling factor based on actual map dims + size_t map_scale_factor = kMapDimensionScaleFactor; + // TODO: If map_scale_factor is guaranteed to be an integer, then remove the following. + // Currently map_scale_factor is of type size_t, but it could be changed to a float + // later. + if (map_scale_factor != floorf(map_scale_factor)) { + recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y); + } else { + recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y, idwTable); + } +#if USE_APPLY_RECOVERY_LUT + Color rgb_hdr = applyRecoveryLUT(rgb_sdr, recovery, recoveryLUT); +#else + Color rgb_hdr = applyRecovery(rgb_sdr, recovery, metadata); +#endif + Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->maxContentBoost); + uint32_t rgba1010102 = colorToRgba1010102(rgb_gamma_hdr); + + size_t pixel_idx = x + y * width; + reinterpret_cast(dest->data)[pixel_idx] = rgba1010102; + } + } + } + }; + + const int threads = std::clamp(GetCPUCoreCount(), 1, 4); + std::vector workers; + for (int th = 0; th < threads - 1; th++) { + workers.push_back(std::thread(applyRecMap)); + } + const int rowStep = threads == 1 ? uncompressed_yuv_420_image->height : kJobSzInRows; + for (int rowStart = 0; rowStart < uncompressed_yuv_420_image->height;) { + int rowEnd = std::min(rowStart + rowStep, uncompressed_yuv_420_image->height); + jobQueue.enqueueJob(rowStart, rowEnd); + rowStart = rowEnd; + } + jobQueue.markQueueForEnd(); + applyRecMap(); + std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); + return NO_ERROR; +} + +status_t JpegR::extractPrimaryImageAndRecoveryMap(jr_compressed_ptr compressed_jpegr_image, + jr_compressed_ptr primary_image, + jr_compressed_ptr recovery_map) { + if (compressed_jpegr_image == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + MessageHandler msg_handler; + std::shared_ptr seg = + DataSegment::Create(DataRange(0, compressed_jpegr_image->length), + static_cast(compressed_jpegr_image->data), + DataSegment::BufferDispositionPolicy::kDontDelete); + DataSegmentDataSource data_source(seg); + JpegInfoBuilder jpeg_info_builder; + jpeg_info_builder.SetImageLimit(2); + JpegScanner jpeg_scanner(&msg_handler); + jpeg_scanner.Run(&data_source, &jpeg_info_builder); + data_source.Reset(); + + if (jpeg_scanner.HasError()) { + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + const auto& jpeg_info = jpeg_info_builder.GetInfo(); + const auto& image_ranges = jpeg_info.GetImageRanges(); + if (image_ranges.empty()) { + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + if (image_ranges.size() != 2) { + // Must be 2 JPEG Images + return ERROR_JPEGR_INVALID_INPUT_TYPE; + } + + if (primary_image != nullptr) { + primary_image->data = static_cast(compressed_jpegr_image->data) + + image_ranges[0].GetBegin(); + primary_image->length = image_ranges[0].GetLength(); + } + + if (recovery_map != nullptr) { + recovery_map->data = static_cast(compressed_jpegr_image->data) + + image_ranges[1].GetBegin(); + recovery_map->length = image_ranges[1].GetLength(); + } + + return NO_ERROR; +} + + +status_t JpegR::extractRecoveryMap(jr_compressed_ptr compressed_jpegr_image, + jr_compressed_ptr dest) { + if (compressed_jpegr_image == nullptr || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + return extractPrimaryImageAndRecoveryMap(compressed_jpegr_image, nullptr, dest); +} + +// JPEG/R structure: +// SOI (ff d8) +// +// (Optional, only if EXIF package is from outside) +// APP1 (ff e1) +// 2 bytes of length (2 + length of exif package) +// EXIF package (this includes the first two bytes representing the package length) +// +// (Required, XMP package) APP1 (ff e1) +// 2 bytes of length (2 + 29 + length of xmp package) +// name space ("http://ns.adobe.com/xap/1.0/\0") +// xmp +// +// (Required) primary image (without the first two bytes (SOI), may have other packages) +// +// (Required) secondary image (the recovery map) +// +// Metadata versions we are using: +// ECMA TR-98 for JFIF marker +// Exif 2.2 spec for EXIF marker +// Adobe XMP spec part 3 for XMP marker +// ICC v4.3 spec for ICC +status_t JpegR::appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image, + jr_compressed_ptr compressed_recovery_map, + jr_exif_ptr exif, + jr_metadata_ptr metadata, + jr_compressed_ptr dest) { + if (compressed_jpeg_image == nullptr + || compressed_recovery_map == nullptr + || metadata == nullptr + || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + int pos = 0; + + // Write SOI + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos)); + + // Write EXIF + if (exif != nullptr) { + const int length = 2 + exif->length; + const uint8_t lengthH = ((length >> 8) & 0xff); + const uint8_t lengthL = (length & 0xff); + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); + JPEGR_CHECK(Write(dest, &lengthH, 1, pos)); + JPEGR_CHECK(Write(dest, &lengthL, 1, pos)); + JPEGR_CHECK(Write(dest, exif->data, exif->length, pos)); + } + + // Prepare and write XMP + { + const string xmp = generateXmp(compressed_recovery_map->length, *metadata); + const string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; + const int nameSpaceLength = nameSpace.size() + 1; // need to count the null terminator + // 2 bytes: representing the length of the package + // 29 bytes: length of name space "http://ns.adobe.com/xap/1.0/\0", + // x bytes: length of xmp packet + const int length = 2 + nameSpaceLength + xmp.size(); + const uint8_t lengthH = ((length >> 8) & 0xff); + const uint8_t lengthL = (length & 0xff); + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); + JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); + JPEGR_CHECK(Write(dest, &lengthH, 1, pos)); + JPEGR_CHECK(Write(dest, &lengthL, 1, pos)); + JPEGR_CHECK(Write(dest, (void*)nameSpace.c_str(), nameSpaceLength, pos)); + JPEGR_CHECK(Write(dest, (void*)xmp.c_str(), xmp.size(), pos)); + } + + // Write primary image + JPEGR_CHECK(Write(dest, + (uint8_t*)compressed_jpeg_image->data + 2, compressed_jpeg_image->length - 2, pos)); + + // Write secondary image + JPEGR_CHECK(Write(dest, compressed_recovery_map->data, compressed_recovery_map->length, pos)); + + // Set back length + dest->length = pos; + + // Done! + return NO_ERROR; +} + +status_t JpegR::toneMap(jr_uncompressed_ptr src, + jr_uncompressed_ptr dest) { + if (src == nullptr || dest == nullptr) { + return ERROR_JPEGR_INVALID_NULL_PTR; + } + + dest->width = src->width; + dest->height = src->height; + + size_t pixel_count = src->width * src->height; + for (size_t y = 0; y < src->height; ++y) { + for (size_t x = 0; x < src->width; ++x) { + size_t pixel_y_idx = x + y * src->width; + size_t pixel_uv_idx = x / 2 + (y / 2) * (src->width / 2); + + uint16_t y_uint = reinterpret_cast(src->data)[pixel_y_idx] + >> 6; + uint16_t u_uint = reinterpret_cast(src->data)[pixel_count + pixel_uv_idx * 2] + >> 6; + uint16_t v_uint = reinterpret_cast(src->data)[pixel_count + pixel_uv_idx * 2 + 1] + >> 6; + + uint8_t* y = &reinterpret_cast(dest->data)[pixel_y_idx]; + uint8_t* u = &reinterpret_cast(dest->data)[pixel_count + pixel_uv_idx]; + uint8_t* v = &reinterpret_cast(dest->data)[pixel_count * 5 / 4 + pixel_uv_idx]; + + *y = static_cast((y_uint >> 2) & 0xff); + *u = static_cast((u_uint >> 2) & 0xff); + *v = static_cast((v_uint >> 2) & 0xff); + } + } + + dest->colorGamut = src->colorGamut; + + return NO_ERROR; +} + +} // namespace android::recoverymap diff --git a/libs/jpegrecoverymap/jpegrutils.cpp b/libs/jpegrecoverymap/jpegrutils.cpp new file mode 100644 index 0000000000..e5323c7b85 --- /dev/null +++ b/libs/jpegrecoverymap/jpegrutils.cpp @@ -0,0 +1,250 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace photos_editing_formats::image_io; +using namespace std; + +namespace android::recoverymap { + +/* + * Helper function used for generating XMP metadata. + * + * @param prefix The prefix part of the name. + * @param suffix The suffix part of the name. + * @return A name of the form "prefix:suffix". + */ +string Name(const string &prefix, const string &suffix) { + std::stringstream ss; + ss << prefix << ":" << suffix; + return ss.str(); +} + +/* + * Helper function used for writing data to destination. + */ +status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int &position) { + if (position + length > destination->maxLength) { + return ERROR_JPEGR_BUFFER_TOO_SMALL; + } + + memcpy((uint8_t*)destination->data + sizeof(uint8_t) * position, source, length); + position += length; + return NO_ERROR; +} + +// Extremely simple XML Handler - just searches for interesting elements +class XMPXmlHandler : public XmlHandler { +public: + + XMPXmlHandler() : XmlHandler() { + gContainerItemState = NotStrarted; + } + + enum ParseState { + NotStrarted, + Started, + Done + }; + + virtual DataMatchResult StartElement(const XmlTokenContext& context) { + string val; + if (context.BuildTokenValue(&val)) { + if (!val.compare(gContainerItemName)) { + gContainerItemState = Started; + } else { + if (gContainerItemState != Done) { + gContainerItemState = NotStrarted; + } + } + } + return context.GetResult(); + } + + virtual DataMatchResult FinishElement(const XmlTokenContext& context) { + if (gContainerItemState == Started) { + gContainerItemState = Done; + lastAttributeName = ""; + } + return context.GetResult(); + } + + virtual DataMatchResult AttributeName(const XmlTokenContext& context) { + string val; + if (gContainerItemState == Started) { + if (context.BuildTokenValue(&val)) { + if (!val.compare(maxContentBoostAttrName)) { + lastAttributeName = maxContentBoostAttrName; + } else { + lastAttributeName = ""; + } + } + } + return context.GetResult(); + } + + virtual DataMatchResult AttributeValue(const XmlTokenContext& context) { + string val; + if (gContainerItemState == Started) { + if (context.BuildTokenValue(&val, true)) { + if (!lastAttributeName.compare(maxContentBoostAttrName)) { + maxContentBoostStr = val; + } + } + } + return context.GetResult(); + } + + bool getMaxContentBoost(float* max_content_boost) { + if (gContainerItemState == Done) { + stringstream ss(maxContentBoostStr); + float val; + if (ss >> val) { + *max_content_boost = val; + return true; + } else { + return false; + } + } else { + return false; + } + } + +private: + static const string gContainerItemName; + static const string maxContentBoostAttrName; + string maxContentBoostStr; + string lastAttributeName; + ParseState gContainerItemState; +}; + +// GContainer XMP constants - URI and namespace prefix +const string kContainerUri = "http://ns.google.com/photos/1.0/container/"; +const string kContainerPrefix = "Container"; + +// GContainer XMP constants - element and attribute names +const string kConDirectory = Name(kContainerPrefix, "Directory"); +const string kConItem = Name(kContainerPrefix, "Item"); + +// GContainer XMP constants - names for XMP handlers +const string XMPXmlHandler::gContainerItemName = kConItem; + +// Item XMP constants - URI and namespace prefix +const string kItemUri = "http://ns.google.com/photos/1.0/container/item/"; +const string kItemPrefix = "Item"; + +// Item XMP constants - element and attribute names +const string kItemLength = Name(kItemPrefix, "Length"); +const string kItemMime = Name(kItemPrefix, "Mime"); +const string kItemSemantic = Name(kItemPrefix, "Semantic"); + +// Item XMP constants - element and attribute values +const string kSemanticPrimary = "Primary"; +const string kSemanticRecoveryMap = "RecoveryMap"; +const string kMimeImageJpeg = "image/jpeg"; + +// RecoveryMap XMP constants - URI and namespace prefix +const string kRecoveryMapUri = "http://ns.google.com/photos/1.0/recoverymap/"; +const string kRecoveryMapPrefix = "RecoveryMap"; + +// RecoveryMap XMP constants - element and attribute names +const string kMapMaxContentBoost = Name(kRecoveryMapPrefix, "MaxContentBoost"); +const string kMapVersion = Name(kRecoveryMapPrefix, "Version"); + +// RecoveryMap XMP constants - names for XMP handlers +const string XMPXmlHandler::maxContentBoostAttrName = kMapMaxContentBoost; + +bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, jpegr_metadata* metadata) { + string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; + + if (xmp_size < nameSpace.size()+2) { + // Data too short + return false; + } + + if (strncmp(reinterpret_cast(xmp_data), nameSpace.c_str(), nameSpace.size())) { + // Not correct namespace + return false; + } + + // Position the pointers to the start of XMP XML portion + xmp_data += nameSpace.size()+1; + xmp_size -= nameSpace.size()+1; + XMPXmlHandler handler; + + // We need to remove tail data until the closing tag. Otherwise parser will throw an error. + while(xmp_data[xmp_size-1]!='>' && xmp_size > 1) { + xmp_size--; + } + + string str(reinterpret_cast(xmp_data), xmp_size); + MessageHandler msg_handler; + unique_ptr rule(new XmlElementRule); + XmlReader reader(&handler, &msg_handler); + reader.StartParse(std::move(rule)); + reader.Parse(str); + reader.FinishParse(); + if (reader.HasErrors()) { + // Parse error + return false; + } + + if (!handler.getMaxContentBoost(&metadata->maxContentBoost)) { + return false; + } + + return true; +} + +string generateXmp(int secondary_image_length, jpegr_metadata& metadata) { + const vector kConDirSeq({kConDirectory, string("rdf:Seq")}); + const vector kLiItem({string("rdf:li"), kConItem}); + + std::stringstream ss; + photos_editing_formats::image_io::XmlWriter writer(ss); + writer.StartWritingElement("x:xmpmeta"); + writer.WriteXmlns("x", "adobe:ns:meta/"); + writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2"); + writer.StartWritingElement("rdf:RDF"); + writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + writer.StartWritingElement("rdf:Description"); + writer.WriteXmlns(kContainerPrefix, kContainerUri); + writer.WriteXmlns(kItemPrefix, kItemUri); + writer.WriteXmlns(kRecoveryMapPrefix, kRecoveryMapUri); + writer.StartWritingElements(kConDirSeq); + size_t item_depth = writer.StartWritingElements(kLiItem); + writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary); + writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); + writer.WriteAttributeNameAndValue(kMapVersion, metadata.version); + writer.WriteAttributeNameAndValue(kMapMaxContentBoost, metadata.maxContentBoost); + writer.FinishWritingElementsToDepth(item_depth); + writer.StartWritingElements(kLiItem); + writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticRecoveryMap); + writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); + writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length); + writer.FinishWriting(); + + return ss.str(); +} + +} // namespace android::recoverymap diff --git a/libs/jpegrecoverymap/recoverymap.cpp b/libs/jpegrecoverymap/recoverymap.cpp deleted file mode 100644 index 349223bb6b..0000000000 --- a/libs/jpegrecoverymap/recoverymap.cpp +++ /dev/null @@ -1,957 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include "SkColorSpace.h" -#include "SkICC.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; -using namespace photos_editing_formats::image_io; - -namespace android::recoverymap { - -#define USE_SRGB_INVOETF_LUT 1 -#define USE_HLG_OETF_LUT 1 -#define USE_PQ_OETF_LUT 1 -#define USE_HLG_INVOETF_LUT 1 -#define USE_PQ_INVOETF_LUT 1 -#define USE_APPLY_RECOVERY_LUT 1 - -#define JPEGR_CHECK(x) \ - { \ - status_t status = (x); \ - if ((status) != NO_ERROR) { \ - return status; \ - } \ - } - -// The current JPEGR version that we encode to -static const uint32_t kJpegrVersion = 1; - -// Map is quarter res / sixteenth size -static const size_t kMapDimensionScaleFactor = 4; -// JPEG block size. -// JPEG encoding / decoding will require 8 x 8 DCT transform. -// Width must be 8 dividable, and height must be 2 dividable. -static const size_t kJpegBlock = 8; -// JPEG compress quality (0 ~ 100) for recovery map -static const int kMapCompressQuality = 85; - -#define CONFIG_MULTITHREAD 1 -int GetCPUCoreCount() { - int cpuCoreCount = 1; -#if CONFIG_MULTITHREAD -#if defined(_SC_NPROCESSORS_ONLN) - cpuCoreCount = sysconf(_SC_NPROCESSORS_ONLN); -#else - // _SC_NPROC_ONLN must be defined... - cpuCoreCount = sysconf(_SC_NPROC_ONLN); -#endif -#endif - return cpuCoreCount; -} - -static const map jrGamut_to_skGamut { - {JPEGR_COLORGAMUT_BT709, SkNamedGamut::kSRGB}, - {JPEGR_COLORGAMUT_P3, SkNamedGamut::kDisplayP3}, - {JPEGR_COLORGAMUT_BT2100, SkNamedGamut::kRec2020}, -}; - -static const map< - recoverymap::jpegr_transfer_function, skcms_TransferFunction> jrTransFunc_to_skTransFunc { - {JPEGR_TF_SRGB, SkNamedTransferFn::kSRGB}, - {JPEGR_TF_LINEAR, SkNamedTransferFn::kLinear}, - {JPEGR_TF_HLG, SkNamedTransferFn::kHLG}, - {JPEGR_TF_PQ, SkNamedTransferFn::kPQ}, -}; - -/* Encode API-0 */ -status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest, - int quality, - jr_exif_ptr exif) { - if (uncompressed_p010_image == nullptr || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - if (quality < 0 || quality > 100) { - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - if (uncompressed_p010_image->width % kJpegBlock != 0 - || uncompressed_p010_image->height % 2 != 0) { - ALOGE("Image size can not be handled: %dx%d", - uncompressed_p010_image->width, uncompressed_p010_image->height); - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - jpegr_metadata metadata; - metadata.version = kJpegrVersion; - - jpegr_uncompressed_struct uncompressed_yuv_420_image; - unique_ptr uncompressed_yuv_420_image_data = make_unique( - uncompressed_p010_image->width * uncompressed_p010_image->height * 3 / 2); - uncompressed_yuv_420_image.data = uncompressed_yuv_420_image_data.get(); - JPEGR_CHECK(toneMap(uncompressed_p010_image, &uncompressed_yuv_420_image)); - - jpegr_uncompressed_struct map; - JPEGR_CHECK(generateRecoveryMap( - &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); - std::unique_ptr map_data; - map_data.reset(reinterpret_cast(map.data)); - - jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); - - sk_sp icc = SkWriteICCProfile( - jrTransFunc_to_skTransFunc.at(JPEGR_TF_SRGB), - jrGamut_to_skGamut.at(uncompressed_yuv_420_image.colorGamut)); - - JpegEncoderHelper jpeg_encoder; - if (!jpeg_encoder.compressImage(uncompressed_yuv_420_image.data, - uncompressed_yuv_420_image.width, - uncompressed_yuv_420_image.height, quality, - icc.get()->data(), icc.get()->size())) { - return ERROR_JPEGR_ENCODE_ERROR; - } - jpegr_compressed_struct jpeg; - jpeg.data = jpeg_encoder.getCompressedImagePtr(); - jpeg.length = jpeg_encoder.getCompressedImageSize(); - - JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, exif, &metadata, dest)); - - return NO_ERROR; -} - -/* Encode API-1 */ -status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_uncompressed_ptr uncompressed_yuv_420_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest, - int quality, - jr_exif_ptr exif) { - if (uncompressed_p010_image == nullptr - || uncompressed_yuv_420_image == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - if (quality < 0 || quality > 100) { - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - if (uncompressed_p010_image->width != uncompressed_yuv_420_image->width - || uncompressed_p010_image->height != uncompressed_yuv_420_image->height) { - return ERROR_JPEGR_RESOLUTION_MISMATCH; - } - - if (uncompressed_p010_image->width % kJpegBlock != 0 - || uncompressed_p010_image->height % 2 != 0) { - ALOGE("Image size can not be handled: %dx%d", - uncompressed_p010_image->width, uncompressed_p010_image->height); - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - jpegr_metadata metadata; - metadata.version = kJpegrVersion; - - jpegr_uncompressed_struct map; - JPEGR_CHECK(generateRecoveryMap( - uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); - std::unique_ptr map_data; - map_data.reset(reinterpret_cast(map.data)); - - jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); - - sk_sp icc = SkWriteICCProfile( - jrTransFunc_to_skTransFunc.at(JPEGR_TF_SRGB), - jrGamut_to_skGamut.at(uncompressed_yuv_420_image->colorGamut)); - - JpegEncoderHelper jpeg_encoder; - if (!jpeg_encoder.compressImage(uncompressed_yuv_420_image->data, - uncompressed_yuv_420_image->width, - uncompressed_yuv_420_image->height, quality, - icc.get()->data(), icc.get()->size())) { - return ERROR_JPEGR_ENCODE_ERROR; - } - jpegr_compressed_struct jpeg; - jpeg.data = jpeg_encoder.getCompressedImagePtr(); - jpeg.length = jpeg_encoder.getCompressedImageSize(); - - JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, exif, &metadata, dest)); - - return NO_ERROR; -} - -/* Encode API-2 */ -status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_compressed_ptr compressed_jpeg_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest) { - if (uncompressed_p010_image == nullptr - || uncompressed_yuv_420_image == nullptr - || compressed_jpeg_image == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - if (uncompressed_p010_image->width != uncompressed_yuv_420_image->width - || uncompressed_p010_image->height != uncompressed_yuv_420_image->height) { - return ERROR_JPEGR_RESOLUTION_MISMATCH; - } - - if (uncompressed_p010_image->width % kJpegBlock != 0 - || uncompressed_p010_image->height % 2 != 0) { - ALOGE("Image size can not be handled: %dx%d", - uncompressed_p010_image->width, uncompressed_p010_image->height); - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - jpegr_metadata metadata; - metadata.version = kJpegrVersion; - - jpegr_uncompressed_struct map; - JPEGR_CHECK(generateRecoveryMap( - uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); - std::unique_ptr map_data; - map_data.reset(reinterpret_cast(map.data)); - - jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); - - JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); - - return NO_ERROR; -} - -/* Encode API-3 */ -status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, - jr_compressed_ptr compressed_jpeg_image, - jpegr_transfer_function hdr_tf, - jr_compressed_ptr dest) { - if (uncompressed_p010_image == nullptr - || compressed_jpeg_image == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - if (uncompressed_p010_image->width % kJpegBlock != 0 - || uncompressed_p010_image->height % 2 != 0) { - ALOGE("Image size can not be handled: %dx%d", - uncompressed_p010_image->width, uncompressed_p010_image->height); - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - JpegDecoderHelper jpeg_decoder; - if (!jpeg_decoder.decompressImage(compressed_jpeg_image->data, compressed_jpeg_image->length)) { - return ERROR_JPEGR_DECODE_ERROR; - } - jpegr_uncompressed_struct uncompressed_yuv_420_image; - uncompressed_yuv_420_image.data = jpeg_decoder.getDecompressedImagePtr(); - uncompressed_yuv_420_image.width = jpeg_decoder.getDecompressedImageWidth(); - uncompressed_yuv_420_image.height = jpeg_decoder.getDecompressedImageHeight(); - uncompressed_yuv_420_image.colorGamut = compressed_jpeg_image->colorGamut; - - if (uncompressed_p010_image->width != uncompressed_yuv_420_image.width - || uncompressed_p010_image->height != uncompressed_yuv_420_image.height) { - return ERROR_JPEGR_RESOLUTION_MISMATCH; - } - - jpegr_metadata metadata; - metadata.version = kJpegrVersion; - - jpegr_uncompressed_struct map; - JPEGR_CHECK(generateRecoveryMap( - &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map)); - std::unique_ptr map_data; - map_data.reset(reinterpret_cast(map.data)); - - jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr compressed_map_data = make_unique(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map)); - - JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); - - return NO_ERROR; -} - -status_t RecoveryMap::getJPEGRInfo(jr_compressed_ptr compressed_jpegr_image, - jr_info_ptr jpegr_info) { - if (compressed_jpegr_image == nullptr || jpegr_info == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - jpegr_compressed_struct primary_image, recovery_map; - JPEGR_CHECK(extractPrimaryImageAndRecoveryMap(compressed_jpegr_image, - &primary_image, &recovery_map)); - - JpegDecoderHelper jpeg_decoder; - if (!jpeg_decoder.getCompressedImageParameters(primary_image.data, primary_image.length, - &jpegr_info->width, &jpegr_info->height, - jpegr_info->iccData, jpegr_info->exifData)) { - return ERROR_JPEGR_DECODE_ERROR; - } - - return NO_ERROR; -} - -/* Decode API */ -status_t RecoveryMap::decodeJPEGR(jr_compressed_ptr compressed_jpegr_image, - jr_uncompressed_ptr dest, - jr_exif_ptr exif, - bool request_sdr) { - if (compressed_jpegr_image == nullptr || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - // TODO: fill EXIF data - (void) exif; - - if (request_sdr) { - JpegDecoderHelper jpeg_decoder; - if (!jpeg_decoder.decompressImage(compressed_jpegr_image->data, compressed_jpegr_image->length, - true)) { - return ERROR_JPEGR_DECODE_ERROR; - } - jpegr_uncompressed_struct uncompressed_rgba_image; - uncompressed_rgba_image.data = jpeg_decoder.getDecompressedImagePtr(); - uncompressed_rgba_image.width = jpeg_decoder.getDecompressedImageWidth(); - uncompressed_rgba_image.height = jpeg_decoder.getDecompressedImageHeight(); - memcpy(dest->data, uncompressed_rgba_image.data, - uncompressed_rgba_image.width * uncompressed_rgba_image.height * 4); - dest->width = uncompressed_rgba_image.width; - dest->height = uncompressed_rgba_image.height; - return NO_ERROR; - } - - jpegr_compressed_struct compressed_map; - jpegr_metadata metadata; - JPEGR_CHECK(extractRecoveryMap(compressed_jpegr_image, &compressed_map)); - - JpegDecoderHelper jpeg_decoder; - if (!jpeg_decoder.decompressImage(compressed_jpegr_image->data, compressed_jpegr_image->length)) { - return ERROR_JPEGR_DECODE_ERROR; - } - - JpegDecoderHelper recovery_map_decoder; - if (!recovery_map_decoder.decompressImage(compressed_map.data, compressed_map.length)) { - return ERROR_JPEGR_DECODE_ERROR; - } - - jpegr_uncompressed_struct map; - map.data = recovery_map_decoder.getDecompressedImagePtr(); - map.width = recovery_map_decoder.getDecompressedImageWidth(); - map.height = recovery_map_decoder.getDecompressedImageHeight(); - - jpegr_uncompressed_struct uncompressed_yuv_420_image; - uncompressed_yuv_420_image.data = jpeg_decoder.getDecompressedImagePtr(); - uncompressed_yuv_420_image.width = jpeg_decoder.getDecompressedImageWidth(); - uncompressed_yuv_420_image.height = jpeg_decoder.getDecompressedImageHeight(); - - if (!getMetadataFromXMP(static_cast(jpeg_decoder.getXMPPtr()), - jpeg_decoder.getXMPSize(), &metadata)) { - return ERROR_JPEGR_DECODE_ERROR; - } - - JPEGR_CHECK(applyRecoveryMap(&uncompressed_yuv_420_image, &map, &metadata, dest)); - return NO_ERROR; -} - -status_t RecoveryMap::compressRecoveryMap(jr_uncompressed_ptr uncompressed_recovery_map, - jr_compressed_ptr dest) { - if (uncompressed_recovery_map == nullptr || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - JpegEncoderHelper jpeg_encoder; - if (!jpeg_encoder.compressImage(uncompressed_recovery_map->data, - uncompressed_recovery_map->width, - uncompressed_recovery_map->height, - kMapCompressQuality, - nullptr, - 0, - true /* isSingleChannel */)) { - return ERROR_JPEGR_ENCODE_ERROR; - } - - if (dest->maxLength < jpeg_encoder.getCompressedImageSize()) { - return ERROR_JPEGR_BUFFER_TOO_SMALL; - } - - memcpy(dest->data, jpeg_encoder.getCompressedImagePtr(), jpeg_encoder.getCompressedImageSize()); - dest->length = jpeg_encoder.getCompressedImageSize(); - dest->colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED; - - return NO_ERROR; -} - -const int kJobSzInRows = 16; -static_assert(kJobSzInRows > 0 && kJobSzInRows % kMapDimensionScaleFactor == 0, - "align job size to kMapDimensionScaleFactor"); - -class JobQueue { - public: - bool dequeueJob(size_t& rowStart, size_t& rowEnd); - void enqueueJob(size_t rowStart, size_t rowEnd); - void markQueueForEnd(); - void reset(); - - private: - bool mQueuedAllJobs = false; - std::deque> mJobs; - std::mutex mMutex; - std::condition_variable mCv; -}; - -bool JobQueue::dequeueJob(size_t& rowStart, size_t& rowEnd) { - std::unique_lock lock{mMutex}; - while (true) { - if (mJobs.empty()) { - if (mQueuedAllJobs) { - return false; - } else { - mCv.wait(lock); - } - } else { - auto it = mJobs.begin(); - rowStart = std::get<0>(*it); - rowEnd = std::get<1>(*it); - mJobs.erase(it); - return true; - } - } - return false; -} - -void JobQueue::enqueueJob(size_t rowStart, size_t rowEnd) { - std::unique_lock lock{mMutex}; - mJobs.push_back(std::make_tuple(rowStart, rowEnd)); - lock.unlock(); - mCv.notify_one(); -} - -void JobQueue::markQueueForEnd() { - std::unique_lock lock{mMutex}; - mQueuedAllJobs = true; -} - -void JobQueue::reset() { - std::unique_lock lock{mMutex}; - mJobs.clear(); - mQueuedAllJobs = false; -} - -status_t RecoveryMap::generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_uncompressed_ptr uncompressed_p010_image, - jpegr_transfer_function hdr_tf, - jr_metadata_ptr metadata, - jr_uncompressed_ptr dest) { - if (uncompressed_yuv_420_image == nullptr - || uncompressed_p010_image == nullptr - || metadata == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - if (uncompressed_yuv_420_image->width != uncompressed_p010_image->width - || uncompressed_yuv_420_image->height != uncompressed_p010_image->height) { - return ERROR_JPEGR_RESOLUTION_MISMATCH; - } - - if (uncompressed_yuv_420_image->colorGamut == JPEGR_COLORGAMUT_UNSPECIFIED - || uncompressed_p010_image->colorGamut == JPEGR_COLORGAMUT_UNSPECIFIED) { - return ERROR_JPEGR_INVALID_COLORGAMUT; - } - - size_t image_width = uncompressed_yuv_420_image->width; - size_t image_height = uncompressed_yuv_420_image->height; - size_t map_width = image_width / kMapDimensionScaleFactor; - size_t map_height = image_height / kMapDimensionScaleFactor; - size_t map_stride = static_cast( - floor((map_width + kJpegBlock - 1) / kJpegBlock)) * kJpegBlock; - size_t map_height_aligned = ((map_height + 1) >> 1) << 1; - - dest->width = map_stride; - dest->height = map_height_aligned; - dest->colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED; - dest->data = new uint8_t[map_stride * map_height_aligned]; - std::unique_ptr map_data; - map_data.reset(reinterpret_cast(dest->data)); - - ColorTransformFn hdrInvOetf = nullptr; - float hdr_white_nits = 0.0f; - switch (hdr_tf) { - case JPEGR_TF_LINEAR: - hdrInvOetf = identityConversion; - break; - case JPEGR_TF_HLG: -#if USE_HLG_INVOETF_LUT - hdrInvOetf = hlgInvOetfLUT; -#else - hdrInvOetf = hlgInvOetf; -#endif - hdr_white_nits = kHlgMaxNits; - break; - case JPEGR_TF_PQ: -#if USE_PQ_INVOETF_LUT - hdrInvOetf = pqInvOetfLUT; -#else - hdrInvOetf = pqInvOetf; -#endif - hdr_white_nits = kPqMaxNits; - break; - default: - // Should be impossible to hit after input validation. - return ERROR_JPEGR_INVALID_TRANS_FUNC; - } - - ColorTransformFn hdrGamutConversionFn = getHdrConversionFn( - uncompressed_yuv_420_image->colorGamut, uncompressed_p010_image->colorGamut); - - ColorCalculationFn luminanceFn = nullptr; - switch (uncompressed_yuv_420_image->colorGamut) { - case JPEGR_COLORGAMUT_BT709: - luminanceFn = srgbLuminance; - break; - case JPEGR_COLORGAMUT_P3: - luminanceFn = p3Luminance; - break; - case JPEGR_COLORGAMUT_BT2100: - luminanceFn = bt2100Luminance; - break; - case JPEGR_COLORGAMUT_UNSPECIFIED: - // Should be impossible to hit after input validation. - return ERROR_JPEGR_INVALID_COLORGAMUT; - } - - std::mutex mutex; - float max_gain = 0.0f; - float min_gain = 1.0f; - const int threads = std::clamp(GetCPUCoreCount(), 1, 4); - size_t rowStep = threads == 1 ? image_height : kJobSzInRows; - JobQueue jobQueue; - - std::function computeMetadata = [uncompressed_p010_image, uncompressed_yuv_420_image, - hdrInvOetf, hdrGamutConversionFn, luminanceFn, - hdr_white_nits, threads, &mutex, &max_gain, &min_gain, - &jobQueue]() -> void { - size_t rowStart, rowEnd; - float max_gain_th = 0.0f; - float min_gain_th = 1.0f; - - while (jobQueue.dequeueJob(rowStart, rowEnd)) { - for (size_t y = rowStart; y < rowEnd; ++y) { - for (size_t x = 0; x < uncompressed_p010_image->width; ++x) { - Color hdr_yuv_gamma = getP010Pixel(uncompressed_p010_image, x, y); - Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); - Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); - hdr_rgb = hdrGamutConversionFn(hdr_rgb); - float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits; - - Color sdr_yuv_gamma = - getYuv420Pixel(uncompressed_yuv_420_image, x, y); - Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma); -#if USE_SRGB_INVOETF_LUT - Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); -#else - Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); -#endif - float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; - - float gain = hdr_y_nits / sdr_y_nits; - max_gain_th = std::max(max_gain_th, gain); - min_gain_th = std::min(min_gain_th, gain); - } - } - } - std::unique_lock lock{mutex}; - max_gain = std::max(max_gain, max_gain_th); - min_gain = std::min(min_gain, min_gain_th); - }; - - std::function generateMap = [uncompressed_yuv_420_image, uncompressed_p010_image, - metadata, dest, hdrInvOetf, hdrGamutConversionFn, - luminanceFn, hdr_white_nits, &jobQueue]() -> void { - size_t rowStart, rowEnd; - size_t dest_map_width = uncompressed_yuv_420_image->width / kMapDimensionScaleFactor; - size_t dest_map_stride = dest->width; - while (jobQueue.dequeueJob(rowStart, rowEnd)) { - for (size_t y = rowStart; y < rowEnd; ++y) { - for (size_t x = 0; x < dest_map_width; ++x) { - Color sdr_yuv_gamma = - sampleYuv420(uncompressed_yuv_420_image, kMapDimensionScaleFactor, x, y); - Color sdr_rgb_gamma = srgbYuvToRgb(sdr_yuv_gamma); -#if USE_SRGB_INVOETF_LUT - Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); -#else - Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); -#endif - float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; - - Color hdr_yuv_gamma = sampleP010(uncompressed_p010_image, kMapDimensionScaleFactor, x, y); - Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); - Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); - hdr_rgb = hdrGamutConversionFn(hdr_rgb); - float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits; - - size_t pixel_idx = x + y * dest_map_stride; - reinterpret_cast(dest->data)[pixel_idx] = - encodeRecovery(sdr_y_nits, hdr_y_nits, metadata); - } - } - } - }; - - std::vector workers; - for (int th = 0; th < threads - 1; th++) { - workers.push_back(std::thread(computeMetadata)); - } - - // compute metadata - for (size_t rowStart = 0; rowStart < image_height;) { - size_t rowEnd = std::min(rowStart + rowStep, image_height); - jobQueue.enqueueJob(rowStart, rowEnd); - rowStart = rowEnd; - } - jobQueue.markQueueForEnd(); - computeMetadata(); - std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); - workers.clear(); - - metadata->maxContentBoost = max_gain; - metadata->minContentBoost = min_gain; - - // generate map - jobQueue.reset(); - for (int th = 0; th < threads - 1; th++) { - workers.push_back(std::thread(generateMap)); - } - - rowStep = (threads == 1 ? image_height : kJobSzInRows) / kMapDimensionScaleFactor; - for (size_t rowStart = 0; rowStart < map_height;) { - size_t rowEnd = std::min(rowStart + rowStep, map_height); - jobQueue.enqueueJob(rowStart, rowEnd); - rowStart = rowEnd; - } - jobQueue.markQueueForEnd(); - generateMap(); - std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); - - map_data.release(); - return NO_ERROR; -} - -status_t RecoveryMap::applyRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image, - jr_uncompressed_ptr uncompressed_recovery_map, - jr_metadata_ptr metadata, - jr_uncompressed_ptr dest) { - if (uncompressed_yuv_420_image == nullptr - || uncompressed_recovery_map == nullptr - || metadata == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - dest->width = uncompressed_yuv_420_image->width; - dest->height = uncompressed_yuv_420_image->height; - ShepardsIDW idwTable(kMapDimensionScaleFactor); - RecoveryLUT recoveryLUT(metadata); - - JobQueue jobQueue; - std::function applyRecMap = [uncompressed_yuv_420_image, uncompressed_recovery_map, - metadata, dest, &jobQueue, &idwTable, - &recoveryLUT]() -> void { - const float hdr_ratio = metadata->maxContentBoost; - size_t width = uncompressed_yuv_420_image->width; - size_t height = uncompressed_yuv_420_image->height; - -#if USE_HLG_OETF_LUT - ColorTransformFn hdrOetf = hlgOetfLUT; -#else - ColorTransformFn hdrOetf = hlgOetf; -#endif - - size_t rowStart, rowEnd; - while (jobQueue.dequeueJob(rowStart, rowEnd)) { - for (size_t y = rowStart; y < rowEnd; ++y) { - for (size_t x = 0; x < width; ++x) { - Color yuv_gamma_sdr = getYuv420Pixel(uncompressed_yuv_420_image, x, y); - Color rgb_gamma_sdr = srgbYuvToRgb(yuv_gamma_sdr); -#if USE_SRGB_INVOETF_LUT - Color rgb_sdr = srgbInvOetfLUT(rgb_gamma_sdr); -#else - Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr); -#endif - float recovery; - // TODO: determine map scaling factor based on actual map dims - size_t map_scale_factor = kMapDimensionScaleFactor; - // TODO: If map_scale_factor is guaranteed to be an integer, then remove the following. - // Currently map_scale_factor is of type size_t, but it could be changed to a float - // later. - if (map_scale_factor != floorf(map_scale_factor)) { - recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y); - } else { - recovery = sampleMap(uncompressed_recovery_map, map_scale_factor, x, y, idwTable); - } -#if USE_APPLY_RECOVERY_LUT - Color rgb_hdr = applyRecoveryLUT(rgb_sdr, recovery, recoveryLUT); -#else - Color rgb_hdr = applyRecovery(rgb_sdr, recovery, metadata); -#endif - Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->maxContentBoost); - uint32_t rgba1010102 = colorToRgba1010102(rgb_gamma_hdr); - - size_t pixel_idx = x + y * width; - reinterpret_cast(dest->data)[pixel_idx] = rgba1010102; - } - } - } - }; - - const int threads = std::clamp(GetCPUCoreCount(), 1, 4); - std::vector workers; - for (int th = 0; th < threads - 1; th++) { - workers.push_back(std::thread(applyRecMap)); - } - const int rowStep = threads == 1 ? uncompressed_yuv_420_image->height : kJobSzInRows; - for (int rowStart = 0; rowStart < uncompressed_yuv_420_image->height;) { - int rowEnd = std::min(rowStart + rowStep, uncompressed_yuv_420_image->height); - jobQueue.enqueueJob(rowStart, rowEnd); - rowStart = rowEnd; - } - jobQueue.markQueueForEnd(); - applyRecMap(); - std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); - return NO_ERROR; -} - -status_t RecoveryMap::extractPrimaryImageAndRecoveryMap(jr_compressed_ptr compressed_jpegr_image, - jr_compressed_ptr primary_image, - jr_compressed_ptr recovery_map) { - if (compressed_jpegr_image == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - MessageHandler msg_handler; - std::shared_ptr seg = - DataSegment::Create(DataRange(0, compressed_jpegr_image->length), - static_cast(compressed_jpegr_image->data), - DataSegment::BufferDispositionPolicy::kDontDelete); - DataSegmentDataSource data_source(seg); - JpegInfoBuilder jpeg_info_builder; - jpeg_info_builder.SetImageLimit(2); - JpegScanner jpeg_scanner(&msg_handler); - jpeg_scanner.Run(&data_source, &jpeg_info_builder); - data_source.Reset(); - - if (jpeg_scanner.HasError()) { - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - const auto& jpeg_info = jpeg_info_builder.GetInfo(); - const auto& image_ranges = jpeg_info.GetImageRanges(); - if (image_ranges.empty()) { - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - if (image_ranges.size() != 2) { - // Must be 2 JPEG Images - return ERROR_JPEGR_INVALID_INPUT_TYPE; - } - - if (primary_image != nullptr) { - primary_image->data = static_cast(compressed_jpegr_image->data) + - image_ranges[0].GetBegin(); - primary_image->length = image_ranges[0].GetLength(); - } - - if (recovery_map != nullptr) { - recovery_map->data = static_cast(compressed_jpegr_image->data) + - image_ranges[1].GetBegin(); - recovery_map->length = image_ranges[1].GetLength(); - } - - return NO_ERROR; -} - - -status_t RecoveryMap::extractRecoveryMap(jr_compressed_ptr compressed_jpegr_image, - jr_compressed_ptr dest) { - if (compressed_jpegr_image == nullptr || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - return extractPrimaryImageAndRecoveryMap(compressed_jpegr_image, nullptr, dest); -} - -// JPEG/R structure: -// SOI (ff d8) -// -// (Optional, only if EXIF package is from outside) -// APP1 (ff e1) -// 2 bytes of length (2 + length of exif package) -// EXIF package (this includes the first two bytes representing the package length) -// -// (Required, XMP package) APP1 (ff e1) -// 2 bytes of length (2 + 29 + length of xmp package) -// name space ("http://ns.adobe.com/xap/1.0/\0") -// xmp -// -// (Required) primary image (without the first two bytes (SOI), may have other packages) -// -// (Required) secondary image (the recovery map) -// -// Metadata versions we are using: -// ECMA TR-98 for JFIF marker -// Exif 2.2 spec for EXIF marker -// Adobe XMP spec part 3 for XMP marker -// ICC v4.3 spec for ICC -status_t RecoveryMap::appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image, - jr_compressed_ptr compressed_recovery_map, - jr_exif_ptr exif, - jr_metadata_ptr metadata, - jr_compressed_ptr dest) { - if (compressed_jpeg_image == nullptr - || compressed_recovery_map == nullptr - || metadata == nullptr - || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - int pos = 0; - - // Write SOI - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos)); - - // Write EXIF - if (exif != nullptr) { - const int length = 2 + exif->length; - const uint8_t lengthH = ((length >> 8) & 0xff); - const uint8_t lengthL = (length & 0xff); - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); - JPEGR_CHECK(Write(dest, &lengthH, 1, pos)); - JPEGR_CHECK(Write(dest, &lengthL, 1, pos)); - JPEGR_CHECK(Write(dest, exif->data, exif->length, pos)); - } - - // Prepare and write XMP - { - const string xmp = generateXmp(compressed_recovery_map->length, *metadata); - const string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; - const int nameSpaceLength = nameSpace.size() + 1; // need to count the null terminator - // 2 bytes: representing the length of the package - // 29 bytes: length of name space "http://ns.adobe.com/xap/1.0/\0", - // x bytes: length of xmp packet - const int length = 2 + nameSpaceLength + xmp.size(); - const uint8_t lengthH = ((length >> 8) & 0xff); - const uint8_t lengthL = (length & 0xff); - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); - JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); - JPEGR_CHECK(Write(dest, &lengthH, 1, pos)); - JPEGR_CHECK(Write(dest, &lengthL, 1, pos)); - JPEGR_CHECK(Write(dest, (void*)nameSpace.c_str(), nameSpaceLength, pos)); - JPEGR_CHECK(Write(dest, (void*)xmp.c_str(), xmp.size(), pos)); - } - - // Write primary image - JPEGR_CHECK(Write(dest, - (uint8_t*)compressed_jpeg_image->data + 2, compressed_jpeg_image->length - 2, pos)); - - // Write secondary image - JPEGR_CHECK(Write(dest, compressed_recovery_map->data, compressed_recovery_map->length, pos)); - - // Set back length - dest->length = pos; - - // Done! - return NO_ERROR; -} - -status_t RecoveryMap::toneMap(jr_uncompressed_ptr src, - jr_uncompressed_ptr dest) { - if (src == nullptr || dest == nullptr) { - return ERROR_JPEGR_INVALID_NULL_PTR; - } - - dest->width = src->width; - dest->height = src->height; - - size_t pixel_count = src->width * src->height; - for (size_t y = 0; y < src->height; ++y) { - for (size_t x = 0; x < src->width; ++x) { - size_t pixel_y_idx = x + y * src->width; - size_t pixel_uv_idx = x / 2 + (y / 2) * (src->width / 2); - - uint16_t y_uint = reinterpret_cast(src->data)[pixel_y_idx] - >> 6; - uint16_t u_uint = reinterpret_cast(src->data)[pixel_count + pixel_uv_idx * 2] - >> 6; - uint16_t v_uint = reinterpret_cast(src->data)[pixel_count + pixel_uv_idx * 2 + 1] - >> 6; - - uint8_t* y = &reinterpret_cast(dest->data)[pixel_y_idx]; - uint8_t* u = &reinterpret_cast(dest->data)[pixel_count + pixel_uv_idx]; - uint8_t* v = &reinterpret_cast(dest->data)[pixel_count * 5 / 4 + pixel_uv_idx]; - - *y = static_cast((y_uint >> 2) & 0xff); - *u = static_cast((u_uint >> 2) & 0xff); - *v = static_cast((v_uint >> 2) & 0xff); - } - } - - dest->colorGamut = src->colorGamut; - - return NO_ERROR; -} - -} // namespace android::recoverymap diff --git a/libs/jpegrecoverymap/recoverymaputils.cpp b/libs/jpegrecoverymap/recoverymaputils.cpp deleted file mode 100644 index 40956bdaba..0000000000 --- a/libs/jpegrecoverymap/recoverymaputils.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include - -using namespace photos_editing_formats::image_io; -using namespace std; - -namespace android::recoverymap { - -/* - * Helper function used for generating XMP metadata. - * - * @param prefix The prefix part of the name. - * @param suffix The suffix part of the name. - * @return A name of the form "prefix:suffix". - */ -string Name(const string &prefix, const string &suffix) { - std::stringstream ss; - ss << prefix << ":" << suffix; - return ss.str(); -} - -/* - * Helper function used for writing data to destination. - */ -status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int &position) { - if (position + length > destination->maxLength) { - return ERROR_JPEGR_BUFFER_TOO_SMALL; - } - - memcpy((uint8_t*)destination->data + sizeof(uint8_t) * position, source, length); - position += length; - return NO_ERROR; -} - -// Extremely simple XML Handler - just searches for interesting elements -class XMPXmlHandler : public XmlHandler { -public: - - XMPXmlHandler() : XmlHandler() { - gContainerItemState = NotStrarted; - } - - enum ParseState { - NotStrarted, - Started, - Done - }; - - virtual DataMatchResult StartElement(const XmlTokenContext& context) { - string val; - if (context.BuildTokenValue(&val)) { - if (!val.compare(gContainerItemName)) { - gContainerItemState = Started; - } else { - if (gContainerItemState != Done) { - gContainerItemState = NotStrarted; - } - } - } - return context.GetResult(); - } - - virtual DataMatchResult FinishElement(const XmlTokenContext& context) { - if (gContainerItemState == Started) { - gContainerItemState = Done; - lastAttributeName = ""; - } - return context.GetResult(); - } - - virtual DataMatchResult AttributeName(const XmlTokenContext& context) { - string val; - if (gContainerItemState == Started) { - if (context.BuildTokenValue(&val)) { - if (!val.compare(maxContentBoostAttrName)) { - lastAttributeName = maxContentBoostAttrName; - } else { - lastAttributeName = ""; - } - } - } - return context.GetResult(); - } - - virtual DataMatchResult AttributeValue(const XmlTokenContext& context) { - string val; - if (gContainerItemState == Started) { - if (context.BuildTokenValue(&val, true)) { - if (!lastAttributeName.compare(maxContentBoostAttrName)) { - maxContentBoostStr = val; - } - } - } - return context.GetResult(); - } - - bool getMaxContentBoost(float* max_content_boost) { - if (gContainerItemState == Done) { - stringstream ss(maxContentBoostStr); - float val; - if (ss >> val) { - *max_content_boost = val; - return true; - } else { - return false; - } - } else { - return false; - } - } - -private: - static const string gContainerItemName; - static const string maxContentBoostAttrName; - string maxContentBoostStr; - string lastAttributeName; - ParseState gContainerItemState; -}; - -// GContainer XMP constants - URI and namespace prefix -const string kContainerUri = "http://ns.google.com/photos/1.0/container/"; -const string kContainerPrefix = "Container"; - -// GContainer XMP constants - element and attribute names -const string kConDirectory = Name(kContainerPrefix, "Directory"); -const string kConItem = Name(kContainerPrefix, "Item"); - -// GContainer XMP constants - names for XMP handlers -const string XMPXmlHandler::gContainerItemName = kConItem; - -// Item XMP constants - URI and namespace prefix -const string kItemUri = "http://ns.google.com/photos/1.0/container/item/"; -const string kItemPrefix = "Item"; - -// Item XMP constants - element and attribute names -const string kItemLength = Name(kItemPrefix, "Length"); -const string kItemMime = Name(kItemPrefix, "Mime"); -const string kItemSemantic = Name(kItemPrefix, "Semantic"); - -// Item XMP constants - element and attribute values -const string kSemanticPrimary = "Primary"; -const string kSemanticRecoveryMap = "RecoveryMap"; -const string kMimeImageJpeg = "image/jpeg"; - -// RecoveryMap XMP constants - URI and namespace prefix -const string kRecoveryMapUri = "http://ns.google.com/photos/1.0/recoverymap/"; -const string kRecoveryMapPrefix = "RecoveryMap"; - -// RecoveryMap XMP constants - element and attribute names -const string kMapMaxContentBoost = Name(kRecoveryMapPrefix, "MaxContentBoost"); -const string kMapVersion = Name(kRecoveryMapPrefix, "Version"); - -// RecoveryMap XMP constants - names for XMP handlers -const string XMPXmlHandler::maxContentBoostAttrName = kMapMaxContentBoost; - -bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, jpegr_metadata* metadata) { - string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; - - if (xmp_size < nameSpace.size()+2) { - // Data too short - return false; - } - - if (strncmp(reinterpret_cast(xmp_data), nameSpace.c_str(), nameSpace.size())) { - // Not correct namespace - return false; - } - - // Position the pointers to the start of XMP XML portion - xmp_data += nameSpace.size()+1; - xmp_size -= nameSpace.size()+1; - XMPXmlHandler handler; - - // We need to remove tail data until the closing tag. Otherwise parser will throw an error. - while(xmp_data[xmp_size-1]!='>' && xmp_size > 1) { - xmp_size--; - } - - string str(reinterpret_cast(xmp_data), xmp_size); - MessageHandler msg_handler; - unique_ptr rule(new XmlElementRule); - XmlReader reader(&handler, &msg_handler); - reader.StartParse(std::move(rule)); - reader.Parse(str); - reader.FinishParse(); - if (reader.HasErrors()) { - // Parse error - return false; - } - - if (!handler.getMaxContentBoost(&metadata->maxContentBoost)) { - return false; - } - - return true; -} - -string generateXmp(int secondary_image_length, jpegr_metadata& metadata) { - const vector kConDirSeq({kConDirectory, string("rdf:Seq")}); - const vector kLiItem({string("rdf:li"), kConItem}); - - std::stringstream ss; - photos_editing_formats::image_io::XmlWriter writer(ss); - writer.StartWritingElement("x:xmpmeta"); - writer.WriteXmlns("x", "adobe:ns:meta/"); - writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2"); - writer.StartWritingElement("rdf:RDF"); - writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); - writer.StartWritingElement("rdf:Description"); - writer.WriteXmlns(kContainerPrefix, kContainerUri); - writer.WriteXmlns(kItemPrefix, kItemUri); - writer.WriteXmlns(kRecoveryMapPrefix, kRecoveryMapUri); - writer.StartWritingElements(kConDirSeq); - size_t item_depth = writer.StartWritingElements(kLiItem); - writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary); - writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); - writer.WriteAttributeNameAndValue(kMapVersion, metadata.version); - writer.WriteAttributeNameAndValue(kMapMaxContentBoost, metadata.maxContentBoost); - writer.FinishWritingElementsToDepth(item_depth); - writer.StartWritingElements(kLiItem); - writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticRecoveryMap); - writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); - writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length); - writer.FinishWriting(); - - return ss.str(); -} - -} // namespace android::recoverymap diff --git a/libs/jpegrecoverymap/tests/Android.bp b/libs/jpegrecoverymap/tests/Android.bp index e416db9e2d..5a4edb2a25 100644 --- a/libs/jpegrecoverymap/tests/Android.bp +++ b/libs/jpegrecoverymap/tests/Android.bp @@ -25,7 +25,7 @@ cc_test { name: "libjpegrecoverymap_test", test_suites: ["device-tests"], srcs: [ - "recoverymap_test.cpp", + "jpegr_test.cpp", "recoverymapmath_test.cpp", ], shared_libs: [ diff --git a/libs/jpegrecoverymap/tests/jpegr_test.cpp b/libs/jpegrecoverymap/tests/jpegr_test.cpp new file mode 100644 index 0000000000..9a0bd3b37c --- /dev/null +++ b/libs/jpegrecoverymap/tests/jpegr_test.cpp @@ -0,0 +1,499 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define RAW_P010_IMAGE "/sdcard/Documents/raw_p010_image.p010" +#define RAW_YUV420_IMAGE "/sdcard/Documents/raw_yuv420_image.yuv420" +#define JPEG_IMAGE "/sdcard/Documents/jpeg_image.jpg" +#define TEST_IMAGE_WIDTH 1280 +#define TEST_IMAGE_HEIGHT 720 +#define DEFAULT_JPEG_QUALITY 90 + +#define SAVE_ENCODING_RESULT true +#define SAVE_DECODING_RESULT true +#define SAVE_INPUT_RGBA true + +namespace android::recoverymap { + +struct Timer { + struct timeval StartingTime; + struct timeval EndingTime; + struct timeval ElapsedMicroseconds; +}; + +void timerStart(Timer *t) { + gettimeofday(&t->StartingTime, nullptr); +} + +void timerStop(Timer *t) { + gettimeofday(&t->EndingTime, nullptr); +} + +int64_t elapsedTime(Timer *t) { + t->ElapsedMicroseconds.tv_sec = t->EndingTime.tv_sec - t->StartingTime.tv_sec; + t->ElapsedMicroseconds.tv_usec = t->EndingTime.tv_usec - t->StartingTime.tv_usec; + return t->ElapsedMicroseconds.tv_sec * 1000000 + t->ElapsedMicroseconds.tv_usec; +} + +static size_t getFileSize(int fd) { + struct stat st; + if (fstat(fd, &st) < 0) { + ALOGW("%s : fstat failed", __func__); + return 0; + } + return st.st_size; // bytes +} + +static bool loadFile(const char filename[], void*& result, int* fileLength) { + int fd = open(filename, O_CLOEXEC); + if (fd < 0) { + return false; + } + int length = getFileSize(fd); + if (length == 0) { + close(fd); + return false; + } + if (fileLength != nullptr) { + *fileLength = length; + } + result = malloc(length); + if (read(fd, result, length) != static_cast(length)) { + close(fd); + return false; + } + close(fd); + return true; +} + +class JpegRTest : public testing::Test { +public: + JpegRTest(); + ~JpegRTest(); + +protected: + virtual void SetUp(); + virtual void TearDown(); + + struct jpegr_uncompressed_struct mRawP010Image; + struct jpegr_uncompressed_struct mRawYuv420Image; + struct jpegr_compressed_struct mJpegImage; +}; + +JpegRTest::JpegRTest() {} +JpegRTest::~JpegRTest() {} + +void JpegRTest::SetUp() {} +void JpegRTest::TearDown() { + free(mRawP010Image.data); + free(mRawYuv420Image.data); + free(mJpegImage.data); +} + +class JpegRBenchmark : public JpegR { +public: + void BenchmarkGenerateRecoveryMap(jr_uncompressed_ptr yuv420Image, jr_uncompressed_ptr p010Image, + jr_metadata_ptr metadata, jr_uncompressed_ptr map); + void BenchmarkApplyRecoveryMap(jr_uncompressed_ptr yuv420Image, jr_uncompressed_ptr map, + jr_metadata_ptr metadata, jr_uncompressed_ptr dest); +private: + const int kProfileCount = 10; +}; + +void JpegRBenchmark::BenchmarkGenerateRecoveryMap(jr_uncompressed_ptr yuv420Image, + jr_uncompressed_ptr p010Image, + jr_metadata_ptr metadata, + jr_uncompressed_ptr map) { + ASSERT_EQ(yuv420Image->width, p010Image->width); + ASSERT_EQ(yuv420Image->height, p010Image->height); + + Timer genRecMapTime; + + timerStart(&genRecMapTime); + for (auto i = 0; i < kProfileCount; i++) { + ASSERT_EQ(OK, generateRecoveryMap( + yuv420Image, p010Image, jpegr_transfer_function::JPEGR_TF_HLG, metadata, map)); + if (i != kProfileCount - 1) delete[] static_cast(map->data); + } + timerStop(&genRecMapTime); + + ALOGE("Generate Recovery Map:- Res = %i x %i, time = %f ms", + yuv420Image->width, yuv420Image->height, + elapsedTime(&genRecMapTime) / (kProfileCount * 1000.f)); + +} + +void JpegRBenchmark::BenchmarkApplyRecoveryMap(jr_uncompressed_ptr yuv420Image, + jr_uncompressed_ptr map, + jr_metadata_ptr metadata, + jr_uncompressed_ptr dest) { + Timer applyRecMapTime; + + timerStart(&applyRecMapTime); + for (auto i = 0; i < kProfileCount; i++) { + ASSERT_EQ(OK, applyRecoveryMap(yuv420Image, map, metadata, dest)); + } + timerStop(&applyRecMapTime); + + ALOGE("Apply Recovery Map:- Res = %i x %i, time = %f ms", + yuv420Image->width, yuv420Image->height, + elapsedTime(&applyRecMapTime) / (kProfileCount * 1000.f)); +} + +TEST_F(JpegRTest, build) { + // Force all of the recovery map lib to be linked by calling all public functions. + JpegR jpegRCodec; + jpegRCodec.encodeJPEGR(nullptr, static_cast(0), nullptr, 0, nullptr); + jpegRCodec.encodeJPEGR(nullptr, nullptr, static_cast(0), + nullptr, 0, nullptr); + jpegRCodec.encodeJPEGR(nullptr, nullptr, nullptr, static_cast(0), + nullptr); + jpegRCodec.encodeJPEGR(nullptr, nullptr, static_cast(0), nullptr); + jpegRCodec.decodeJPEGR(nullptr, nullptr, nullptr, false); +} + +TEST_F(JpegRTest, writeXmpThenRead) { + jpegr_metadata metadata_expected; + metadata_expected.maxContentBoost = 1.25; + int length_expected = 1000; + const std::string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; + const int nameSpaceLength = nameSpace.size() + 1; // need to count the null terminator + + std::string xmp = generateXmp(1000, metadata_expected); + + std::vector xmpData; + xmpData.reserve(nameSpaceLength + xmp.size()); + xmpData.insert(xmpData.end(), reinterpret_cast(nameSpace.c_str()), + reinterpret_cast(nameSpace.c_str()) + nameSpaceLength); + xmpData.insert(xmpData.end(), reinterpret_cast(xmp.c_str()), + reinterpret_cast(xmp.c_str()) + xmp.size()); + + jpegr_metadata metadata_read; + EXPECT_TRUE(getMetadataFromXMP(xmpData.data(), xmpData.size(), &metadata_read)); + ASSERT_EQ(metadata_expected.maxContentBoost, metadata_read.maxContentBoost); +} + +/* Test Encode API-0 and decode */ +TEST_F(JpegRTest, encodeFromP010ThenDecode) { + int ret; + + // Load input files. + if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawP010Image.width = TEST_IMAGE_WIDTH; + mRawP010Image.height = TEST_IMAGE_HEIGHT; + mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + + JpegR jpegRCodec; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); + jpegR.data = malloc(jpegR.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, DEFAULT_JPEG_QUALITY, nullptr); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_ENCODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)jpegR.data, jpegR.length); + } + + jpegr_uncompressed_struct decodedJpegR; + int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; + decodedJpegR.data = malloc(decodedJpegRSize); + ret = jpegRCodec.decodeJPEGR(&jpegR, &decodedJpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_DECODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); + } + + free(jpegR.data); + free(decodedJpegR.data); +} + +/* Test Encode API-1 and decode */ +TEST_F(JpegRTest, encodeFromRawHdrAndSdrThenDecode) { + int ret; + + // Load input files. + if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawP010Image.width = TEST_IMAGE_WIDTH; + mRawP010Image.height = TEST_IMAGE_HEIGHT; + mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + + if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawYuv420Image.width = TEST_IMAGE_WIDTH; + mRawYuv420Image.height = TEST_IMAGE_HEIGHT; + mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + + JpegR jpegRCodec; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); + jpegR.data = malloc(jpegR.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010Image, &mRawYuv420Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, + DEFAULT_JPEG_QUALITY, nullptr); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_ENCODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)jpegR.data, jpegR.length); + } + + jpegr_uncompressed_struct decodedJpegR; + int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; + decodedJpegR.data = malloc(decodedJpegRSize); + ret = jpegRCodec.decodeJPEGR(&jpegR, &decodedJpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_DECODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); + } + + free(jpegR.data); + free(decodedJpegR.data); +} + +/* Test Encode API-2 and decode */ +TEST_F(JpegRTest, encodeFromRawHdrAndSdrAndJpegThenDecode) { + int ret; + + // Load input files. + if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawP010Image.width = TEST_IMAGE_WIDTH; + mRawP010Image.height = TEST_IMAGE_HEIGHT; + mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + + if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawYuv420Image.width = TEST_IMAGE_WIDTH; + mRawYuv420Image.height = TEST_IMAGE_HEIGHT; + mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + + if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) { + FAIL() << "Load file " << JPEG_IMAGE << " failed"; + } + mJpegImage.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + + JpegR jpegRCodec; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); + jpegR.data = malloc(jpegR.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010Image, &mRawYuv420Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_ENCODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)jpegR.data, jpegR.length); + } + + jpegr_uncompressed_struct decodedJpegR; + int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; + decodedJpegR.data = malloc(decodedJpegRSize); + ret = jpegRCodec.decodeJPEGR(&jpegR, &decodedJpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_DECODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); + } + + free(jpegR.data); + free(decodedJpegR.data); +} + +/* Test Encode API-3 and decode */ +TEST_F(JpegRTest, encodeFromJpegThenDecode) { + int ret; + + // Load input files. + if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawP010Image.width = TEST_IMAGE_WIDTH; + mRawP010Image.height = TEST_IMAGE_HEIGHT; + mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + + if (SAVE_INPUT_RGBA) { + size_t rgbaSize = mRawP010Image.width * mRawP010Image.height * sizeof(uint32_t); + uint32_t *data = (uint32_t *)malloc(rgbaSize); + + for (size_t y = 0; y < mRawP010Image.height; ++y) { + for (size_t x = 0; x < mRawP010Image.width; ++x) { + Color hdr_yuv_gamma = getP010Pixel(&mRawP010Image, x, y); + Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); + uint32_t rgba1010102 = colorToRgba1010102(hdr_rgb_gamma); + size_t pixel_idx = x + y * mRawP010Image.width; + reinterpret_cast(data)[pixel_idx] = rgba1010102; + } + } + + // Output image data to file + std::string filePath = "/sdcard/Documents/input_from_p010.rgb10"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)data, rgbaSize); + free(data); + } + if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) { + FAIL() << "Load file " << JPEG_IMAGE << " failed"; + } + mJpegImage.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + + JpegR jpegRCodec; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); + jpegR.data = malloc(jpegR.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_ENCODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)jpegR.data, jpegR.length); + } + + jpegr_uncompressed_struct decodedJpegR; + int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; + decodedJpegR.data = malloc(decodedJpegRSize); + ret = jpegRCodec.decodeJPEGR(&jpegR, &decodedJpegR); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + if (SAVE_DECODING_RESULT) { + // Output image data to file + std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; + std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); + if (!imageFile.is_open()) { + ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); + } + imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); + } + + free(jpegR.data); + free(decodedJpegR.data); +} + +TEST_F(JpegRTest, ProfileRecoveryMapFuncs) { + const size_t kWidth = TEST_IMAGE_WIDTH; + const size_t kHeight = TEST_IMAGE_HEIGHT; + + // Load input files. + if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawP010Image.width = kWidth; + mRawP010Image.height = kHeight; + mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + + if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + mRawYuv420Image.width = kWidth; + mRawYuv420Image.height = kHeight; + mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + + JpegRBenchmark benchmark; + + jpegr_metadata metadata = { .version = 1, + .maxContentBoost = 8.0f, + .minContentBoost = 1.0f / 8.0f }; + + jpegr_uncompressed_struct map = { .data = NULL, + .width = 0, + .height = 0, + .colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED }; + + benchmark.BenchmarkGenerateRecoveryMap(&mRawYuv420Image, &mRawP010Image, &metadata, &map); + + const int dstSize = mRawYuv420Image.width * mRawYuv420Image.height * 4; + auto bufferDst = std::make_unique(dstSize); + jpegr_uncompressed_struct dest = { .data = bufferDst.get(), + .width = 0, + .height = 0, + .colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED }; + + benchmark.BenchmarkApplyRecoveryMap(&mRawYuv420Image, &map, &metadata, &dest); +} + +} // namespace android::recoverymap \ No newline at end of file diff --git a/libs/jpegrecoverymap/tests/recoverymap_test.cpp b/libs/jpegrecoverymap/tests/recoverymap_test.cpp deleted file mode 100644 index 1b73d94057..0000000000 --- a/libs/jpegrecoverymap/tests/recoverymap_test.cpp +++ /dev/null @@ -1,499 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#define RAW_P010_IMAGE "/sdcard/Documents/raw_p010_image.p010" -#define RAW_YUV420_IMAGE "/sdcard/Documents/raw_yuv420_image.yuv420" -#define JPEG_IMAGE "/sdcard/Documents/jpeg_image.jpg" -#define TEST_IMAGE_WIDTH 1280 -#define TEST_IMAGE_HEIGHT 720 -#define DEFAULT_JPEG_QUALITY 90 - -#define SAVE_ENCODING_RESULT true -#define SAVE_DECODING_RESULT true -#define SAVE_INPUT_RGBA true - -namespace android::recoverymap { - -struct Timer { - struct timeval StartingTime; - struct timeval EndingTime; - struct timeval ElapsedMicroseconds; -}; - -void timerStart(Timer *t) { - gettimeofday(&t->StartingTime, nullptr); -} - -void timerStop(Timer *t) { - gettimeofday(&t->EndingTime, nullptr); -} - -int64_t elapsedTime(Timer *t) { - t->ElapsedMicroseconds.tv_sec = t->EndingTime.tv_sec - t->StartingTime.tv_sec; - t->ElapsedMicroseconds.tv_usec = t->EndingTime.tv_usec - t->StartingTime.tv_usec; - return t->ElapsedMicroseconds.tv_sec * 1000000 + t->ElapsedMicroseconds.tv_usec; -} - -static size_t getFileSize(int fd) { - struct stat st; - if (fstat(fd, &st) < 0) { - ALOGW("%s : fstat failed", __func__); - return 0; - } - return st.st_size; // bytes -} - -static bool loadFile(const char filename[], void*& result, int* fileLength) { - int fd = open(filename, O_CLOEXEC); - if (fd < 0) { - return false; - } - int length = getFileSize(fd); - if (length == 0) { - close(fd); - return false; - } - if (fileLength != nullptr) { - *fileLength = length; - } - result = malloc(length); - if (read(fd, result, length) != static_cast(length)) { - close(fd); - return false; - } - close(fd); - return true; -} - -class RecoveryMapTest : public testing::Test { -public: - RecoveryMapTest(); - ~RecoveryMapTest(); - -protected: - virtual void SetUp(); - virtual void TearDown(); - - struct jpegr_uncompressed_struct mRawP010Image; - struct jpegr_uncompressed_struct mRawYuv420Image; - struct jpegr_compressed_struct mJpegImage; -}; - -RecoveryMapTest::RecoveryMapTest() {} -RecoveryMapTest::~RecoveryMapTest() {} - -void RecoveryMapTest::SetUp() {} -void RecoveryMapTest::TearDown() { - free(mRawP010Image.data); - free(mRawYuv420Image.data); - free(mJpegImage.data); -} - -class RecoveryMapBenchmark : public RecoveryMap { -public: - void BenchmarkGenerateRecoveryMap(jr_uncompressed_ptr yuv420Image, jr_uncompressed_ptr p010Image, - jr_metadata_ptr metadata, jr_uncompressed_ptr map); - void BenchmarkApplyRecoveryMap(jr_uncompressed_ptr yuv420Image, jr_uncompressed_ptr map, - jr_metadata_ptr metadata, jr_uncompressed_ptr dest); -private: - const int kProfileCount = 10; -}; - -void RecoveryMapBenchmark::BenchmarkGenerateRecoveryMap(jr_uncompressed_ptr yuv420Image, - jr_uncompressed_ptr p010Image, - jr_metadata_ptr metadata, - jr_uncompressed_ptr map) { - ASSERT_EQ(yuv420Image->width, p010Image->width); - ASSERT_EQ(yuv420Image->height, p010Image->height); - - Timer genRecMapTime; - - timerStart(&genRecMapTime); - for (auto i = 0; i < kProfileCount; i++) { - ASSERT_EQ(OK, generateRecoveryMap( - yuv420Image, p010Image, jpegr_transfer_function::JPEGR_TF_HLG, metadata, map)); - if (i != kProfileCount - 1) delete[] static_cast(map->data); - } - timerStop(&genRecMapTime); - - ALOGE("Generate Recovery Map:- Res = %i x %i, time = %f ms", - yuv420Image->width, yuv420Image->height, - elapsedTime(&genRecMapTime) / (kProfileCount * 1000.f)); - -} - -void RecoveryMapBenchmark::BenchmarkApplyRecoveryMap(jr_uncompressed_ptr yuv420Image, - jr_uncompressed_ptr map, - jr_metadata_ptr metadata, - jr_uncompressed_ptr dest) { - Timer applyRecMapTime; - - timerStart(&applyRecMapTime); - for (auto i = 0; i < kProfileCount; i++) { - ASSERT_EQ(OK, applyRecoveryMap(yuv420Image, map, metadata, dest)); - } - timerStop(&applyRecMapTime); - - ALOGE("Apply Recovery Map:- Res = %i x %i, time = %f ms", - yuv420Image->width, yuv420Image->height, - elapsedTime(&applyRecMapTime) / (kProfileCount * 1000.f)); -} - -TEST_F(RecoveryMapTest, build) { - // Force all of the recovery map lib to be linked by calling all public functions. - RecoveryMap recovery_map; - recovery_map.encodeJPEGR(nullptr, static_cast(0), nullptr, 0, nullptr); - recovery_map.encodeJPEGR(nullptr, nullptr, static_cast(0), - nullptr, 0, nullptr); - recovery_map.encodeJPEGR(nullptr, nullptr, nullptr, static_cast(0), - nullptr); - recovery_map.encodeJPEGR(nullptr, nullptr, static_cast(0), nullptr); - recovery_map.decodeJPEGR(nullptr, nullptr, nullptr, false); -} - -TEST_F(RecoveryMapTest, writeXmpThenRead) { - jpegr_metadata metadata_expected; - metadata_expected.maxContentBoost = 1.25; - int length_expected = 1000; - const std::string nameSpace = "http://ns.adobe.com/xap/1.0/\0"; - const int nameSpaceLength = nameSpace.size() + 1; // need to count the null terminator - - std::string xmp = generateXmp(1000, metadata_expected); - - std::vector xmpData; - xmpData.reserve(nameSpaceLength + xmp.size()); - xmpData.insert(xmpData.end(), reinterpret_cast(nameSpace.c_str()), - reinterpret_cast(nameSpace.c_str()) + nameSpaceLength); - xmpData.insert(xmpData.end(), reinterpret_cast(xmp.c_str()), - reinterpret_cast(xmp.c_str()) + xmp.size()); - - jpegr_metadata metadata_read; - EXPECT_TRUE(getMetadataFromXMP(xmpData.data(), xmpData.size(), &metadata_read)); - ASSERT_EQ(metadata_expected.maxContentBoost, metadata_read.maxContentBoost); -} - -/* Test Encode API-0 and decode */ -TEST_F(RecoveryMapTest, encodeFromP010ThenDecode) { - int ret; - - // Load input files. - if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawP010Image.width = TEST_IMAGE_WIDTH; - mRawP010Image.height = TEST_IMAGE_HEIGHT; - mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; - - RecoveryMap recoveryMap; - - jpegr_compressed_struct jpegR; - jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); - jpegR.data = malloc(jpegR.maxLength); - ret = recoveryMap.encodeJPEGR( - &mRawP010Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, DEFAULT_JPEG_QUALITY, nullptr); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_ENCODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)jpegR.data, jpegR.length); - } - - jpegr_uncompressed_struct decodedJpegR; - int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; - decodedJpegR.data = malloc(decodedJpegRSize); - ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_DECODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); - } - - free(jpegR.data); - free(decodedJpegR.data); -} - -/* Test Encode API-1 and decode */ -TEST_F(RecoveryMapTest, encodeFromRawHdrAndSdrThenDecode) { - int ret; - - // Load input files. - if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawP010Image.width = TEST_IMAGE_WIDTH; - mRawP010Image.height = TEST_IMAGE_HEIGHT; - mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; - - if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawYuv420Image.width = TEST_IMAGE_WIDTH; - mRawYuv420Image.height = TEST_IMAGE_HEIGHT; - mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; - - RecoveryMap recoveryMap; - - jpegr_compressed_struct jpegR; - jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); - jpegR.data = malloc(jpegR.maxLength); - ret = recoveryMap.encodeJPEGR( - &mRawP010Image, &mRawYuv420Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, - DEFAULT_JPEG_QUALITY, nullptr); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_ENCODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)jpegR.data, jpegR.length); - } - - jpegr_uncompressed_struct decodedJpegR; - int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; - decodedJpegR.data = malloc(decodedJpegRSize); - ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_DECODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); - } - - free(jpegR.data); - free(decodedJpegR.data); -} - -/* Test Encode API-2 and decode */ -TEST_F(RecoveryMapTest, encodeFromRawHdrAndSdrAndJpegThenDecode) { - int ret; - - // Load input files. - if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawP010Image.width = TEST_IMAGE_WIDTH; - mRawP010Image.height = TEST_IMAGE_HEIGHT; - mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; - - if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawYuv420Image.width = TEST_IMAGE_WIDTH; - mRawYuv420Image.height = TEST_IMAGE_HEIGHT; - mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; - - if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) { - FAIL() << "Load file " << JPEG_IMAGE << " failed"; - } - mJpegImage.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; - - RecoveryMap recoveryMap; - - jpegr_compressed_struct jpegR; - jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); - jpegR.data = malloc(jpegR.maxLength); - ret = recoveryMap.encodeJPEGR( - &mRawP010Image, &mRawYuv420Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_ENCODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)jpegR.data, jpegR.length); - } - - jpegr_uncompressed_struct decodedJpegR; - int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; - decodedJpegR.data = malloc(decodedJpegRSize); - ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_DECODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); - } - - free(jpegR.data); - free(decodedJpegR.data); -} - -/* Test Encode API-3 and decode */ -TEST_F(RecoveryMapTest, encodeFromJpegThenDecode) { - int ret; - - // Load input files. - if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawP010Image.width = TEST_IMAGE_WIDTH; - mRawP010Image.height = TEST_IMAGE_HEIGHT; - mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; - - if (SAVE_INPUT_RGBA) { - size_t rgbaSize = mRawP010Image.width * mRawP010Image.height * sizeof(uint32_t); - uint32_t *data = (uint32_t *)malloc(rgbaSize); - - for (size_t y = 0; y < mRawP010Image.height; ++y) { - for (size_t x = 0; x < mRawP010Image.width; ++x) { - Color hdr_yuv_gamma = getP010Pixel(&mRawP010Image, x, y); - Color hdr_rgb_gamma = bt2100YuvToRgb(hdr_yuv_gamma); - uint32_t rgba1010102 = colorToRgba1010102(hdr_rgb_gamma); - size_t pixel_idx = x + y * mRawP010Image.width; - reinterpret_cast(data)[pixel_idx] = rgba1010102; - } - } - - // Output image data to file - std::string filePath = "/sdcard/Documents/input_from_p010.rgb10"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)data, rgbaSize); - free(data); - } - if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) { - FAIL() << "Load file " << JPEG_IMAGE << " failed"; - } - mJpegImage.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; - - RecoveryMap recoveryMap; - - jpegr_compressed_struct jpegR; - jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); - jpegR.data = malloc(jpegR.maxLength); - ret = recoveryMap.encodeJPEGR( - &mRawP010Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_ENCODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)jpegR.data, jpegR.length); - } - - jpegr_uncompressed_struct decodedJpegR; - int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4; - decodedJpegR.data = malloc(decodedJpegRSize); - ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR); - if (ret != OK) { - FAIL() << "Error code is " << ret; - } - if (SAVE_DECODING_RESULT) { - // Output image data to file - std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10"; - std::ofstream imageFile(filePath.c_str(), std::ofstream::binary); - if (!imageFile.is_open()) { - ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str()); - } - imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize); - } - - free(jpegR.data); - free(decodedJpegR.data); -} - -TEST_F(RecoveryMapTest, ProfileRecoveryMapFuncs) { - const size_t kWidth = TEST_IMAGE_WIDTH; - const size_t kHeight = TEST_IMAGE_HEIGHT; - - // Load input files. - if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawP010Image.width = kWidth; - mRawP010Image.height = kHeight; - mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; - - if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) { - FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; - } - mRawYuv420Image.width = kWidth; - mRawYuv420Image.height = kHeight; - mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; - - RecoveryMapBenchmark benchmark; - - jpegr_metadata metadata = { .version = 1, - .maxContentBoost = 8.0f, - .minContentBoost = 1.0f / 8.0f }; - - jpegr_uncompressed_struct map = { .data = NULL, - .width = 0, - .height = 0, - .colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED }; - - benchmark.BenchmarkGenerateRecoveryMap(&mRawYuv420Image, &mRawP010Image, &metadata, &map); - - const int dstSize = mRawYuv420Image.width * mRawYuv420Image.height * 4; - auto bufferDst = std::make_unique(dstSize); - jpegr_uncompressed_struct dest = { .data = bufferDst.get(), - .width = 0, - .height = 0, - .colorGamut = JPEGR_COLORGAMUT_UNSPECIFIED }; - - benchmark.BenchmarkApplyRecoveryMap(&mRawYuv420Image, &map, &metadata, &dest); -} - -} // namespace android::recoverymap -- cgit v1.2.3-59-g8ed1b