/* * 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. */ #ifdef _WIN32 #include #include #else #include #endif #include #include #include #include #include #include "ultrahdr/editorhelper.h" #include "ultrahdr/gainmapmetadata.h" #include "ultrahdr/ultrahdrcommon.h" #include "ultrahdr/jpegr.h" #include "ultrahdr/icc.h" #include "ultrahdr/multipictureformat.h" #include "image_io/base/data_segment_data_source.h" #include "image_io/jpeg/jpeg_info.h" #include "image_io/jpeg/jpeg_info_builder.h" #include "image_io/jpeg/jpeg_marker.h" #include "image_io/jpeg/jpeg_scanner.h" using namespace std; using namespace photos_editing_formats::image_io; namespace ultrahdr { #ifdef UHDR_ENABLE_GLES uhdr_error_info_t applyGainMapGLES(uhdr_raw_image_t* sdr_intent, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* gainmap_metadata, uhdr_color_transfer_t output_ct, float display_boost, uhdr_raw_image_t* dest, uhdr_opengl_ctxt_t* opengl_ctxt); #endif // Gain map metadata static const bool kWriteXmpMetadata = true; static const bool kWriteIso21496_1Metadata = false; static const string kXmpNameSpace = "http://ns.adobe.com/xap/1.0/"; static const string kIsoNameSpace = "urn:iso:std:iso:ts:21496:-1"; static_assert(kWriteXmpMetadata || kWriteIso21496_1Metadata, "Must write gain map metadata in XMP format, or iso 21496-1 format, or both."); class JobQueue { public: bool dequeueJob(unsigned int& rowStart, unsigned int& rowEnd); void enqueueJob(unsigned int rowStart, unsigned int rowEnd); void markQueueForEnd(); void reset(); private: bool mQueuedAllJobs = false; std::deque> mJobs; std::mutex mMutex; std::condition_variable mCv; }; bool JobQueue::dequeueJob(unsigned int& rowStart, unsigned int& rowEnd) { std::unique_lock lock{mMutex}; while (true) { if (mJobs.empty()) { if (mQueuedAllJobs) { return false; } else { mCv.wait_for(lock, std::chrono::milliseconds(100)); } } 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(unsigned int rowStart, unsigned int 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; lock.unlock(); mCv.notify_all(); } void JobQueue::reset() { std::unique_lock lock{mMutex}; mJobs.clear(); mQueuedAllJobs = false; } /* * MessageWriter implementation for ALOG functions. */ class AlogMessageWriter : public MessageWriter { public: void WriteMessage(const Message& message) override { std::string log = GetFormattedMessage(message); ALOGD("%s", log.c_str()); } }; unsigned int GetCPUCoreCount() { return (std::max)(1u, std::thread::hardware_concurrency()); } JpegR::JpegR(void* uhdrGLESCtxt, int mapDimensionScaleFactor, int mapCompressQuality, bool useMultiChannelGainMap, float gamma, uhdr_enc_preset_t preset, float minContentBoost, float maxContentBoost, float targetDispPeakBrightness) { mUhdrGLESCtxt = uhdrGLESCtxt; mMapDimensionScaleFactor = mapDimensionScaleFactor; mMapCompressQuality = mapCompressQuality; mUseMultiChannelGainMap = useMultiChannelGainMap; mGamma = gamma; mEncPreset = preset; mMinContentBoost = minContentBoost; mMaxContentBoost = maxContentBoost; mTargetDispPeakBrightness = targetDispPeakBrightness; } /* * Helper function copies the JPEG image from without EXIF. * * @param pDest destination of the data to be written. * @param pSource source of data being written. * @param exif_pos position of the EXIF package, which is aligned with jpegdecoder.getEXIFPos(). * (4 bytes offset to FF sign, the byte after FF E1 XX XX ). * @param exif_size exif size without the initial 4 bytes, aligned with jpegdecoder.getEXIFSize(). */ static void copyJpegWithoutExif(uhdr_compressed_image_t* pDest, uhdr_compressed_image_t* pSource, size_t exif_pos, size_t exif_size) { const size_t exif_offset = 4; // exif_pos has 4 bytes offset to the FF sign pDest->data_sz = pSource->data_sz - exif_size - exif_offset; pDest->data = new uint8_t[pDest->data_sz]; pDest->capacity = pDest->data_sz; pDest->cg = pSource->cg; pDest->ct = pSource->ct; pDest->range = pSource->range; memcpy(pDest->data, pSource->data, exif_pos - exif_offset); memcpy((uint8_t*)pDest->data + exif_pos - exif_offset, (uint8_t*)pSource->data + exif_pos + exif_size, pSource->data_sz - exif_pos - exif_size); } /* Encode API-0 */ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { uhdr_img_fmt_t sdr_intent_fmt; if (hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010) { sdr_intent_fmt = UHDR_IMG_FMT_12bppYCbCr420; } else if (hdr_intent->fmt == UHDR_IMG_FMT_30bppYCbCr444) { sdr_intent_fmt = UHDR_IMG_FMT_24bppYCbCr444; } else if (hdr_intent->fmt == UHDR_IMG_FMT_32bppRGBA1010102 || hdr_intent->fmt == UHDR_IMG_FMT_64bppRGBAHalfFloat) { sdr_intent_fmt = UHDR_IMG_FMT_32bppRGBA8888; } else { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "unsupported hdr intent color format %d", hdr_intent->fmt); return status; } std::unique_ptr sdr_intent = std::make_unique( sdr_intent_fmt, UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, hdr_intent->w, hdr_intent->h, 64); // tone map UHDR_ERR_CHECK(toneMap(hdr_intent, sdr_intent.get())); // If hdr intent is tonemapped internally, it is observed from quality pov, // generateGainMapOnePass() is sufficient mEncPreset = UHDR_USAGE_REALTIME; // overriding the config option // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; UHDR_ERR_CHECK(generateGainMap(sdr_intent.get(), hdr_intent, &metadata, gainmap, /* sdr_is_601 */ false, /* use_luminance */ false)); // compress gain map JpegEncoderHelper jpeg_enc_obj_gm; UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm)); uhdr_compressed_image_t gainmap_compressed = jpeg_enc_obj_gm.getCompressedImage(); std::shared_ptr icc = IccHelper::writeIccProfile(UHDR_CT_SRGB, sdr_intent->cg); // compress sdr image std::unique_ptr sdr_intent_yuv_ext; uhdr_raw_image_t* sdr_intent_yuv = sdr_intent.get(); if (isPixelFormatRgb(sdr_intent->fmt)) { #if (defined(UHDR_ENABLE_INTRINSICS) && (defined(__ARM_NEON__) || defined(__ARM_NEON))) sdr_intent_yuv_ext = convert_raw_input_to_ycbcr_neon(sdr_intent.get()); #else sdr_intent_yuv_ext = convert_raw_input_to_ycbcr(sdr_intent.get()); #endif sdr_intent_yuv = sdr_intent_yuv_ext.get(); } JpegEncoderHelper jpeg_enc_obj_sdr; UHDR_ERR_CHECK( jpeg_enc_obj_sdr.compressImage(sdr_intent_yuv, quality, icc->getData(), icc->getLength())); uhdr_compressed_image_t sdr_intent_compressed = jpeg_enc_obj_sdr.getCompressedImage(); sdr_intent_compressed.cg = sdr_intent_yuv->cg; // append gain map, no ICC since JPEG encode already did it UHDR_ERR_CHECK(appendGainMap(&sdr_intent_compressed, &gainmap_compressed, exif, /* icc */ nullptr, /* icc size */ 0, &metadata, dest)); return g_no_error; } /* Encode API-1 */ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; UHDR_ERR_CHECK(generateGainMap(sdr_intent, hdr_intent, &metadata, gainmap)); // compress gain map JpegEncoderHelper jpeg_enc_obj_gm; UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm)); uhdr_compressed_image_t gainmap_compressed = jpeg_enc_obj_gm.getCompressedImage(); std::shared_ptr icc = IccHelper::writeIccProfile(UHDR_CT_SRGB, sdr_intent->cg); std::unique_ptr sdr_intent_yuv_ext; uhdr_raw_image_t* sdr_intent_yuv = sdr_intent; if (isPixelFormatRgb(sdr_intent->fmt)) { #if (defined(UHDR_ENABLE_INTRINSICS) && (defined(__ARM_NEON__) || defined(__ARM_NEON))) sdr_intent_yuv_ext = convert_raw_input_to_ycbcr_neon(sdr_intent); #else sdr_intent_yuv_ext = convert_raw_input_to_ycbcr(sdr_intent); #endif sdr_intent_yuv = sdr_intent_yuv_ext.get(); } // convert to bt601 YUV encoding for JPEG encode #if (defined(UHDR_ENABLE_INTRINSICS) && (defined(__ARM_NEON__) || defined(__ARM_NEON))) UHDR_ERR_CHECK(convertYuv_neon(sdr_intent_yuv, sdr_intent_yuv->cg, UHDR_CG_DISPLAY_P3)); #else UHDR_ERR_CHECK(convertYuv(sdr_intent_yuv, sdr_intent_yuv->cg, UHDR_CG_DISPLAY_P3)); #endif // compress sdr image JpegEncoderHelper jpeg_enc_obj_sdr; UHDR_ERR_CHECK( jpeg_enc_obj_sdr.compressImage(sdr_intent_yuv, quality, icc->getData(), icc->getLength())); uhdr_compressed_image_t sdr_intent_compressed = jpeg_enc_obj_sdr.getCompressedImage(); sdr_intent_compressed.cg = sdr_intent_yuv->cg; // append gain map, no ICC since JPEG encode already did it UHDR_ERR_CHECK(appendGainMap(&sdr_intent_compressed, &gainmap_compressed, exif, /* icc */ nullptr, /* icc size */ 0, &metadata, dest)); return g_no_error; } /* Encode API-2 */ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* sdr_intent_compressed, uhdr_compressed_image_t* dest) { JpegDecoderHelper jpeg_dec_obj_sdr; UHDR_ERR_CHECK(jpeg_dec_obj_sdr.decompressImage(sdr_intent_compressed->data, sdr_intent_compressed->data_sz, PARSE_STREAM)); if (hdr_intent->w != jpeg_dec_obj_sdr.getDecompressedImageWidth() || hdr_intent->h != jpeg_dec_obj_sdr.getDecompressedImageHeight()) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf( status.detail, sizeof status.detail, "sdr intent resolution %dx%d and compressed image sdr intent resolution %dx%d do not match", sdr_intent->w, sdr_intent->h, (int)jpeg_dec_obj_sdr.getDecompressedImageWidth(), (int)jpeg_dec_obj_sdr.getDecompressedImageHeight()); return status; } // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; UHDR_ERR_CHECK(generateGainMap(sdr_intent, hdr_intent, &metadata, gainmap)); // compress gain map JpegEncoderHelper jpeg_enc_obj_gm; UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm)); uhdr_compressed_image_t gainmap_compressed = jpeg_enc_obj_gm.getCompressedImage(); return encodeJPEGR(sdr_intent_compressed, &gainmap_compressed, &metadata, dest); } /* Encode API-3 */ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* sdr_intent_compressed, uhdr_compressed_image_t* dest) { // decode input jpeg, gamut is going to be bt601. JpegDecoderHelper jpeg_dec_obj_sdr; UHDR_ERR_CHECK(jpeg_dec_obj_sdr.decompressImage(sdr_intent_compressed->data, sdr_intent_compressed->data_sz)); uhdr_raw_image_t sdr_intent = jpeg_dec_obj_sdr.getDecompressedImage(); if (jpeg_dec_obj_sdr.getICCSize() > 0) { uhdr_color_gamut_t cg = IccHelper::readIccColorGamut(jpeg_dec_obj_sdr.getICCPtr(), jpeg_dec_obj_sdr.getICCSize()); if (cg == UHDR_CG_UNSPECIFIED || (sdr_intent_compressed->cg != UHDR_CG_UNSPECIFIED && sdr_intent_compressed->cg != cg)) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "configured color gamut %d does not match with color gamut specified in icc box %d", sdr_intent_compressed->cg, cg); return status; } sdr_intent.cg = cg; } else { if (sdr_intent_compressed->cg <= UHDR_CG_UNSPECIFIED || sdr_intent_compressed->cg > UHDR_CG_BT_2100) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized 420 color gamut %d", sdr_intent_compressed->cg); return status; } sdr_intent.cg = sdr_intent_compressed->cg; } if (hdr_intent->w != sdr_intent.w || hdr_intent->h != sdr_intent.h) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "sdr intent resolution %dx%d and hdr intent resolution %dx%d do not match", sdr_intent.w, sdr_intent.h, hdr_intent->w, hdr_intent->h); return status; } // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; UHDR_ERR_CHECK( generateGainMap(&sdr_intent, hdr_intent, &metadata, gainmap, true /* sdr_is_601 */)); // compress gain map JpegEncoderHelper jpeg_enc_obj_gm; UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm)); uhdr_compressed_image_t gainmap_compressed = jpeg_enc_obj_gm.getCompressedImage(); return encodeJPEGR(sdr_intent_compressed, &gainmap_compressed, &metadata, dest); } /* Encode API-4 */ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_compressed_image_t* base_img_compressed, uhdr_compressed_image_t* gainmap_img_compressed, uhdr_gainmap_metadata_ext_t* metadata, uhdr_compressed_image_t* dest) { // We just want to check if ICC is present, so don't do a full decode. Note, // this doesn't verify that the ICC is valid. JpegDecoderHelper decoder; UHDR_ERR_CHECK(decoder.parseImage(base_img_compressed->data, base_img_compressed->data_sz)); // Add ICC if not already present. if (decoder.getICCSize() > 0) { UHDR_ERR_CHECK(appendGainMap(base_img_compressed, gainmap_img_compressed, /* exif */ nullptr, /* icc */ nullptr, /* icc size */ 0, metadata, dest)); } else { if (base_img_compressed->cg <= UHDR_CG_UNSPECIFIED || base_img_compressed->cg > UHDR_CG_BT_2100) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized 420 color gamut %d", base_img_compressed->cg); return status; } std::shared_ptr newIcc = IccHelper::writeIccProfile(UHDR_CT_SRGB, base_img_compressed->cg); UHDR_ERR_CHECK(appendGainMap(base_img_compressed, gainmap_img_compressed, /* exif */ nullptr, newIcc->getData(), newIcc->getLength(), metadata, dest)); } return g_no_error; } uhdr_error_info_t JpegR::convertYuv(uhdr_raw_image_t* image, uhdr_color_gamut_t src_encoding, uhdr_color_gamut_t dst_encoding) { const std::array* coeffs_ptr = nullptr; uhdr_error_info_t status = g_no_error; switch (src_encoding) { case UHDR_CG_BT_709: switch (dst_encoding) { case UHDR_CG_BT_709: return status; case UHDR_CG_DISPLAY_P3: coeffs_ptr = &kYuvBt709ToBt601; break; case UHDR_CG_BT_2100: coeffs_ptr = &kYuvBt709ToBt2100; break; default: status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized dest color gamut %d", dst_encoding); return status; } break; case UHDR_CG_DISPLAY_P3: switch (dst_encoding) { case UHDR_CG_BT_709: coeffs_ptr = &kYuvBt601ToBt709; break; case UHDR_CG_DISPLAY_P3: return status; case UHDR_CG_BT_2100: coeffs_ptr = &kYuvBt601ToBt2100; break; default: status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized dest color gamut %d", dst_encoding); return status; } break; case UHDR_CG_BT_2100: switch (dst_encoding) { case UHDR_CG_BT_709: coeffs_ptr = &kYuvBt2100ToBt709; break; case UHDR_CG_DISPLAY_P3: coeffs_ptr = &kYuvBt2100ToBt601; break; case UHDR_CG_BT_2100: return status; default: status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized dest color gamut %d", dst_encoding); return status; } break; default: status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unrecognized src color gamut %d", src_encoding); return status; } if (image->fmt == UHDR_IMG_FMT_12bppYCbCr420) { transformYuv420(image, *coeffs_ptr); } else if (image->fmt == UHDR_IMG_FMT_24bppYCbCr444) { transformYuv444(image, *coeffs_ptr); } else { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for performing gamut conversion for color format %d", image->fmt); return status; } return status; } uhdr_error_info_t JpegR::compressGainMap(uhdr_raw_image_t* gainmap_img, JpegEncoderHelper* jpeg_enc_obj) { return jpeg_enc_obj->compressImage(gainmap_img, mMapCompressQuality, nullptr, 0); } uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_image_t* hdr_intent, uhdr_gainmap_metadata_ext_t* gainmap_metadata, std::unique_ptr& gainmap_img, bool sdr_is_601, bool use_luminance) { uhdr_error_info_t status = g_no_error; if (sdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCr444 && sdr_intent->fmt != UHDR_IMG_FMT_16bppYCbCr422 && sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420 && sdr_intent->fmt != UHDR_IMG_FMT_32bppRGBA8888) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "generate gainmap method expects sdr intent color format to be one of " "{UHDR_IMG_FMT_24bppYCbCr444, UHDR_IMG_FMT_16bppYCbCr422, " "UHDR_IMG_FMT_12bppYCbCr420, UHDR_IMG_FMT_32bppRGBA8888}. Received %d", sdr_intent->fmt); return status; } if (hdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCrP010 && hdr_intent->fmt != UHDR_IMG_FMT_30bppYCbCr444 && hdr_intent->fmt != UHDR_IMG_FMT_32bppRGBA1010102 && hdr_intent->fmt != UHDR_IMG_FMT_64bppRGBAHalfFloat) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "generate gainmap method expects hdr intent color format to be one of " "{UHDR_IMG_FMT_24bppYCbCrP010, UHDR_IMG_FMT_30bppYCbCr444, " "UHDR_IMG_FMT_32bppRGBA1010102, UHDR_IMG_FMT_64bppRGBAHalfFloat}. Received %d", hdr_intent->fmt); return status; } /*if (mUseMultiChannelGainMap) { if (!kWriteIso21496_1Metadata || kWriteXmpMetadata) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Multi-channel gain map is only supported for ISO 21496-1 metadata"); return status; } }*/ ColorTransformFn hdrInvOetf = getInverseOetfFn(hdr_intent->ct); if (hdrInvOetf == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for converting transfer characteristics %d to linear", hdr_intent->ct); return status; } LuminanceFn hdrLuminanceFn = getLuminanceFn(hdr_intent->cg); if (hdrLuminanceFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for calculating luminance for color gamut %d", hdr_intent->cg); return status; } SceneToDisplayLuminanceFn hdrOotfFn = getOotfFn(hdr_intent->ct); if (hdrOotfFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for calculating Ootf for color transfer %d", hdr_intent->ct); return status; } float hdr_white_nits = getReferenceDisplayPeakLuminanceInNits(hdr_intent->ct); if (hdr_white_nits == -1.0f) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "received invalid peak brightness %f nits for hdr reference display with color " "transfer %d ", hdr_white_nits, hdr_intent->ct); return status; } ColorTransformFn hdrGamutConversionFn = getGamutConversionFn(sdr_intent->cg, hdr_intent->cg); if (hdrGamutConversionFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for gamut conversion from %d to %d", hdr_intent->cg, sdr_intent->cg); return status; } ColorTransformFn sdrYuvToRgbFn = getYuvToRgbFn(sdr_intent->cg); if (sdrYuvToRgbFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for converting yuv to rgb for color gamut %d", sdr_intent->cg); return status; } ColorTransformFn hdrYuvToRgbFn = getYuvToRgbFn(hdr_intent->cg); if (hdrYuvToRgbFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for converting yuv to rgb for color gamut %d", hdr_intent->cg); return status; } LuminanceFn luminanceFn = getLuminanceFn(sdr_intent->cg); if (luminanceFn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for computing luminance for color gamut %d", sdr_intent->cg); return status; } SamplePixelFn sdr_sample_pixel_fn = getSamplePixelFn(sdr_intent->fmt); if (sdr_sample_pixel_fn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for reading pixels for color format %d", sdr_intent->fmt); return status; } SamplePixelFn hdr_sample_pixel_fn = getSamplePixelFn(hdr_intent->fmt); if (hdr_sample_pixel_fn == nullptr) { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for reading pixels for color format %d", hdr_intent->fmt); return status; } if (sdr_is_601) { sdrYuvToRgbFn = p3YuvToRgb; } unsigned int image_width = sdr_intent->w; unsigned int image_height = sdr_intent->h; unsigned int map_width = image_width / mMapDimensionScaleFactor; unsigned int map_height = image_height / mMapDimensionScaleFactor; if (map_width == 0 || map_height == 0) { int scaleFactor = (std::min)(image_width, image_height); scaleFactor = (scaleFactor >= DCTSIZE) ? (scaleFactor / DCTSIZE) : 1; ALOGW( "configured gainmap scale factor is resulting in gainmap width and/or height to be zero, " "image width %u, image height %u, scale factor %d. Modifying gainmap scale factor to %d ", image_width, image_height, mMapDimensionScaleFactor, scaleFactor); setMapDimensionScaleFactor(scaleFactor); map_width = image_width / mMapDimensionScaleFactor; map_height = image_height / mMapDimensionScaleFactor; } gainmap_img = std::make_unique( mUseMultiChannelGainMap ? UHDR_IMG_FMT_24bppRGB888 : UHDR_IMG_FMT_8bppYCbCr400, UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, map_width, map_height, 64); uhdr_raw_image_ext_t* dest = gainmap_img.get(); auto generateGainMapOnePass = [this, sdr_intent, hdr_intent, gainmap_metadata, dest, map_height, hdrInvOetf, hdrLuminanceFn, hdrOotfFn, hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn, hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn, hdr_white_nits, use_luminance]() -> void { gainmap_metadata->max_content_boost = hdr_white_nits / kSdrWhiteNits; gainmap_metadata->min_content_boost = 1.0f; gainmap_metadata->gamma = mGamma; gainmap_metadata->offset_sdr = 0.0f; gainmap_metadata->offset_hdr = 0.0f; gainmap_metadata->hdr_capacity_min = 1.0f; if (this->mTargetDispPeakBrightness != -1.0f) { gainmap_metadata->hdr_capacity_max = this->mTargetDispPeakBrightness / kSdrWhiteNits; } else { gainmap_metadata->hdr_capacity_max = gainmap_metadata->max_content_boost; } float log2MinBoost = log2(gainmap_metadata->min_content_boost); float log2MaxBoost = log2(gainmap_metadata->max_content_boost); const int threads = (std::min)(GetCPUCoreCount(), 4u); const int jobSizeInRows = 1; unsigned int rowStep = threads == 1 ? map_height : jobSizeInRows; JobQueue jobQueue; std::function generateMap = [this, sdr_intent, hdr_intent, gainmap_metadata, dest, hdrInvOetf, hdrLuminanceFn, hdrOotfFn, hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn, hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn, hdr_white_nits, log2MinBoost, log2MaxBoost, use_luminance, &jobQueue]() -> void { unsigned int rowStart, rowEnd; const bool isHdrIntentRgb = isPixelFormatRgb(hdr_intent->fmt); const bool isSdrIntentRgb = isPixelFormatRgb(sdr_intent->fmt); const float hdrSampleToNitsFactor = hdr_intent->ct == UHDR_CT_LINEAR ? kSdrWhiteNits : hdr_white_nits; ColorTransformFn clampPixel = hdr_intent->ct == UHDR_CT_LINEAR ? static_cast(clampPixelFloatLinear) : static_cast(clampPixelFloat); while (jobQueue.dequeueJob(rowStart, rowEnd)) { for (size_t y = rowStart; y < rowEnd; ++y) { for (size_t x = 0; x < dest->w; ++x) { Color sdr_rgb_gamma; if (isSdrIntentRgb) { sdr_rgb_gamma = sdr_sample_pixel_fn(sdr_intent, mMapDimensionScaleFactor, x, y); } else { Color sdr_yuv_gamma = sdr_sample_pixel_fn(sdr_intent, mMapDimensionScaleFactor, x, y); sdr_rgb_gamma = sdrYuvToRgbFn(sdr_yuv_gamma); } // We are assuming the SDR input is always sRGB transfer. #if USE_SRGB_INVOETF_LUT Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); #else Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); #endif Color hdr_rgb_gamma; if (isHdrIntentRgb) { hdr_rgb_gamma = hdr_sample_pixel_fn(hdr_intent, mMapDimensionScaleFactor, x, y); } else { Color hdr_yuv_gamma = hdr_sample_pixel_fn(hdr_intent, mMapDimensionScaleFactor, x, y); hdr_rgb_gamma = hdrYuvToRgbFn(hdr_yuv_gamma); } Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); hdr_rgb = hdrOotfFn(hdr_rgb, hdrLuminanceFn); hdr_rgb = hdrGamutConversionFn(hdr_rgb); hdr_rgb = clampPixel(hdr_rgb); if (mUseMultiChannelGainMap) { Color sdr_rgb_nits = sdr_rgb * kSdrWhiteNits; Color hdr_rgb_nits = hdr_rgb * hdrSampleToNitsFactor; size_t pixel_idx = (x + y * dest->stride[UHDR_PLANE_PACKED]) * 3; reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx] = encodeGain( sdr_rgb_nits.r, hdr_rgb_nits.r, gainmap_metadata, log2MinBoost, log2MaxBoost); reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx + 1] = encodeGain(sdr_rgb_nits.g, hdr_rgb_nits.g, gainmap_metadata, log2MinBoost, log2MaxBoost); reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx + 2] = encodeGain(sdr_rgb_nits.b, hdr_rgb_nits.b, gainmap_metadata, log2MinBoost, log2MaxBoost); } else { float sdr_y_nits; float hdr_y_nits; if (use_luminance) { sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; hdr_y_nits = luminanceFn(hdr_rgb) * hdrSampleToNitsFactor; } else { sdr_y_nits = fmax(sdr_rgb.r, fmax(sdr_rgb.g, sdr_rgb.b)) * kSdrWhiteNits; hdr_y_nits = fmax(hdr_rgb.r, fmax(hdr_rgb.g, hdr_rgb.b)) * hdrSampleToNitsFactor; } size_t pixel_idx = x + y * dest->stride[UHDR_PLANE_Y]; reinterpret_cast(dest->planes[UHDR_PLANE_Y])[pixel_idx] = encodeGain(sdr_y_nits, hdr_y_nits, gainmap_metadata, log2MinBoost, log2MaxBoost); } } } } }; // generate map std::vector workers; for (int th = 0; th < threads - 1; th++) { workers.push_back(std::thread(generateMap)); } for (unsigned int rowStart = 0; rowStart < map_height;) { unsigned int 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(); }); }; auto generateGainMapTwoPass = [this, sdr_intent, hdr_intent, gainmap_metadata, dest, map_width, map_height, hdrInvOetf, hdrLuminanceFn, hdrOotfFn, hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn, hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn, hdr_white_nits, use_luminance]() -> void { uhdr_memory_block_t gainmap_mem((size_t)map_width * map_height * sizeof(float) * (mUseMultiChannelGainMap ? 3 : 1)); float* gainmap_data = reinterpret_cast(gainmap_mem.m_buffer.get()); float gainmap_min[3] = {127.0f, 127.0f, 127.0f}; float gainmap_max[3] = {-128.0f, -128.0f, -128.0f}; std::mutex gainmap_minmax; const int threads = (std::min)(GetCPUCoreCount(), 4u); const int jobSizeInRows = 1; unsigned int rowStep = threads == 1 ? map_height : jobSizeInRows; JobQueue jobQueue; std::function generateMap = [this, sdr_intent, hdr_intent, gainmap_data, map_width, hdrInvOetf, hdrLuminanceFn, hdrOotfFn, hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn, hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn, hdr_white_nits, use_luminance, &gainmap_min, &gainmap_max, &gainmap_minmax, &jobQueue]() -> void { unsigned int rowStart, rowEnd; const bool isHdrIntentRgb = isPixelFormatRgb(hdr_intent->fmt); const bool isSdrIntentRgb = isPixelFormatRgb(sdr_intent->fmt); const float hdrSampleToNitsFactor = hdr_intent->ct == UHDR_CT_LINEAR ? kSdrWhiteNits : hdr_white_nits; ColorTransformFn clampPixel = hdr_intent->ct == UHDR_CT_LINEAR ? static_cast(clampPixelFloatLinear) : static_cast(clampPixelFloat); float gainmap_min_th[3] = {127.0f, 127.0f, 127.0f}; float gainmap_max_th[3] = {-128.0f, -128.0f, -128.0f}; while (jobQueue.dequeueJob(rowStart, rowEnd)) { for (size_t y = rowStart; y < rowEnd; ++y) { for (size_t x = 0; x < map_width; ++x) { Color sdr_rgb_gamma; if (isSdrIntentRgb) { sdr_rgb_gamma = sdr_sample_pixel_fn(sdr_intent, mMapDimensionScaleFactor, x, y); } else { Color sdr_yuv_gamma = sdr_sample_pixel_fn(sdr_intent, mMapDimensionScaleFactor, x, y); sdr_rgb_gamma = sdrYuvToRgbFn(sdr_yuv_gamma); } // We are assuming the SDR input is always sRGB transfer. #if USE_SRGB_INVOETF_LUT Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma); #else Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma); #endif Color hdr_rgb_gamma; if (isHdrIntentRgb) { hdr_rgb_gamma = hdr_sample_pixel_fn(hdr_intent, mMapDimensionScaleFactor, x, y); } else { Color hdr_yuv_gamma = hdr_sample_pixel_fn(hdr_intent, mMapDimensionScaleFactor, x, y); hdr_rgb_gamma = hdrYuvToRgbFn(hdr_yuv_gamma); } Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); hdr_rgb = hdrOotfFn(hdr_rgb, hdrLuminanceFn); hdr_rgb = hdrGamutConversionFn(hdr_rgb); hdr_rgb = clampPixel(hdr_rgb); if (mUseMultiChannelGainMap) { Color sdr_rgb_nits = sdr_rgb * kSdrWhiteNits; Color hdr_rgb_nits = hdr_rgb * hdrSampleToNitsFactor; size_t pixel_idx = (x + y * map_width) * 3; gainmap_data[pixel_idx] = computeGain(sdr_rgb_nits.r, hdr_rgb_nits.r); gainmap_data[pixel_idx + 1] = computeGain(sdr_rgb_nits.g, hdr_rgb_nits.g); gainmap_data[pixel_idx + 2] = computeGain(sdr_rgb_nits.b, hdr_rgb_nits.b); for (int i = 0; i < 3; i++) { gainmap_min_th[i] = (std::min)(gainmap_data[pixel_idx + i], gainmap_min_th[i]); gainmap_max_th[i] = (std::max)(gainmap_data[pixel_idx + i], gainmap_max_th[i]); } } else { float sdr_y_nits; float hdr_y_nits; if (use_luminance) { sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits; hdr_y_nits = luminanceFn(hdr_rgb) * hdrSampleToNitsFactor; } else { sdr_y_nits = fmax(sdr_rgb.r, fmax(sdr_rgb.g, sdr_rgb.b)) * kSdrWhiteNits; hdr_y_nits = fmax(hdr_rgb.r, fmax(hdr_rgb.g, hdr_rgb.b)) * hdrSampleToNitsFactor; } size_t pixel_idx = x + y * map_width; gainmap_data[pixel_idx] = computeGain(sdr_y_nits, hdr_y_nits); gainmap_min_th[0] = (std::min)(gainmap_data[pixel_idx], gainmap_min_th[0]); gainmap_max_th[0] = (std::max)(gainmap_data[pixel_idx], gainmap_max_th[0]); } } } } { std::unique_lock lock{gainmap_minmax}; for (int index = 0; index < (mUseMultiChannelGainMap ? 3 : 1); index++) { gainmap_min[index] = (std::min)(gainmap_min[index], gainmap_min_th[index]); gainmap_max[index] = (std::max)(gainmap_max[index], gainmap_max_th[index]); } } }; // generate map std::vector workers; for (int th = 0; th < threads - 1; th++) { workers.push_back(std::thread(generateMap)); } for (unsigned int rowStart = 0; rowStart < map_height;) { unsigned int 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(); }); float min_content_boost_log2 = gainmap_min[0]; float max_content_boost_log2 = gainmap_max[0]; for (int index = 1; index < (mUseMultiChannelGainMap ? 3 : 1); index++) { min_content_boost_log2 = (std::min)(gainmap_min[index], min_content_boost_log2); max_content_boost_log2 = (std::max)(gainmap_max[index], max_content_boost_log2); } // -13.0 emphirically is a small enough gain factor that is capable of representing hdr // black from any sdr luminance. Allowing further excursion might not offer any benefit and on // the downside can cause bigger error during affine map and inverse map. min_content_boost_log2 = (std::max)(-13.0f, min_content_boost_log2); if (this->mMaxContentBoost != FLT_MAX) { float suggestion = log2(this->mMaxContentBoost); max_content_boost_log2 = (std::min)(max_content_boost_log2, suggestion); } if (this->mMinContentBoost != FLT_MIN) { float suggestion = log2(this->mMinContentBoost); min_content_boost_log2 = (std::max)(min_content_boost_log2, suggestion); } if (fabs(max_content_boost_log2 - min_content_boost_log2) < FLT_EPSILON) { max_content_boost_log2 += 0.1; // to avoid div by zero during affine transform } std::function encodeMap = [this, gainmap_data, map_width, dest, min_content_boost_log2, max_content_boost_log2, &jobQueue]() -> void { unsigned int rowStart, rowEnd; while (jobQueue.dequeueJob(rowStart, rowEnd)) { if (mUseMultiChannelGainMap) { for (size_t j = rowStart; j < rowEnd; j++) { size_t dst_pixel_idx = j * dest->stride[UHDR_PLANE_PACKED] * 3; size_t src_pixel_idx = j * map_width * 3; for (size_t i = 0; i < map_width * 3; i++) { reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[dst_pixel_idx + i] = affineMapGain(gainmap_data[src_pixel_idx + i], min_content_boost_log2, max_content_boost_log2, this->mGamma); } } } else { for (size_t j = rowStart; j < rowEnd; j++) { size_t dst_pixel_idx = j * dest->stride[UHDR_PLANE_Y]; size_t src_pixel_idx = j * map_width; for (size_t i = 0; i < map_width; i++) { reinterpret_cast(dest->planes[UHDR_PLANE_Y])[dst_pixel_idx + i] = affineMapGain(gainmap_data[src_pixel_idx + i], min_content_boost_log2, max_content_boost_log2, this->mGamma); } } } } }; workers.clear(); jobQueue.reset(); rowStep = threads == 1 ? map_height : 1; for (int th = 0; th < threads - 1; th++) { workers.push_back(std::thread(encodeMap)); } for (unsigned int rowStart = 0; rowStart < map_height;) { unsigned int rowEnd = (std::min)(rowStart + rowStep, map_height); jobQueue.enqueueJob(rowStart, rowEnd); rowStart = rowEnd; } jobQueue.markQueueForEnd(); encodeMap(); std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); gainmap_metadata->max_content_boost = exp2(max_content_boost_log2); gainmap_metadata->min_content_boost = exp2(min_content_boost_log2); gainmap_metadata->gamma = this->mGamma; gainmap_metadata->offset_sdr = 0.0f; gainmap_metadata->offset_hdr = 0.0f; gainmap_metadata->hdr_capacity_min = 1.0f; if (this->mTargetDispPeakBrightness != -1.0f) { gainmap_metadata->hdr_capacity_max = this->mTargetDispPeakBrightness / kSdrWhiteNits; } else { gainmap_metadata->hdr_capacity_max = hdr_white_nits / kSdrWhiteNits; } }; if (mEncPreset == UHDR_USAGE_REALTIME) { generateGainMapOnePass(); } else { generateGainMapTwoPass(); } return status; } // JPEG/R structure: // SOI (ff d8) // // (Optional, if EXIF package is from outside (Encode API-0 API-1), or if EXIF package presents // in the JPEG input (Encode API-2, API-3, API-4)) // 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, ISO 21496-1 metadata, version only) APP2 (ff e2) // 2 bytes of length // name space (""urn:iso:std:iso:ts:21496:-1\0") // 2 bytes minimum_version: (00 00) // 2 bytes writer_version: (00 00) // // (Required, MPF package) APP2 (ff e2) // 2 bytes of length // MPF // // (Required) primary image (without the first two bytes (SOI) and EXIF, may have other packages) // // SOI (ff d8) // // (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, ISO 21496-1 metadata) APP2 (ff e2) // 2 bytes of length // name space (""urn:iso:std:iso:ts:21496:-1\0") // metadata // // (Required) secondary image (the gain map, without the first two bytes (SOI)) // // 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 uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compressed, uhdr_compressed_image_t* gainmap_compressed, uhdr_mem_block_t* pExif, void* pIcc, size_t icc_size, uhdr_gainmap_metadata_ext_t* metadata, uhdr_compressed_image_t* dest) { const size_t xmpNameSpaceLength = kXmpNameSpace.size() + 1; // need to count the null terminator const size_t isoNameSpaceLength = kIsoNameSpace.size() + 1; // need to count the null terminator ///////////////////////////////////////////////////////////////////////////////////////////////// // calculate secondary image length first, because the length will be written into the primary // // image xmp // ///////////////////////////////////////////////////////////////////////////////////////////////// // XMP string xmp_secondary; size_t xmp_secondary_length; if (kWriteXmpMetadata) { xmp_secondary = generateXmpForSecondaryImage(*metadata); // xmp_secondary_length = 2 bytes representing the length of the package + // + xmpNameSpaceLength = 29 bytes length // + length of xmp packet = xmp_secondary.size() xmp_secondary_length = 2 + xmpNameSpaceLength + xmp_secondary.size(); } // ISO uhdr_gainmap_metadata_frac iso_secondary_metadata; std::vector iso_secondary_data; size_t iso_secondary_length; if (kWriteIso21496_1Metadata) { UHDR_ERR_CHECK(uhdr_gainmap_metadata_frac::gainmapMetadataFloatToFraction( metadata, &iso_secondary_metadata)); UHDR_ERR_CHECK(uhdr_gainmap_metadata_frac::encodeGainmapMetadata(&iso_secondary_metadata, iso_secondary_data)); // iso_secondary_length = 2 bytes representing the length of the package + // + isoNameSpaceLength = 28 bytes length // + length of iso metadata packet = iso_secondary_data.size() iso_secondary_length = 2 + isoNameSpaceLength + iso_secondary_data.size(); } size_t secondary_image_size = 2 /* 2 bytes length of APP1 sign */ + gainmap_compressed->data_sz; if (kWriteXmpMetadata) { secondary_image_size += xmp_secondary_length; } if (kWriteIso21496_1Metadata) { secondary_image_size += iso_secondary_length; } // Check if EXIF package presents in the JPEG input. // If so, extract and remove the EXIF package. JpegDecoderHelper decoder; UHDR_ERR_CHECK(decoder.parseImage(sdr_intent_compressed->data, sdr_intent_compressed->data_sz)); uhdr_mem_block_t exif_from_jpg; exif_from_jpg.data = nullptr; exif_from_jpg.data_sz = 0; uhdr_compressed_image_t new_jpg_image; new_jpg_image.data = nullptr; new_jpg_image.data_sz = 0; new_jpg_image.capacity = 0; new_jpg_image.cg = UHDR_CG_UNSPECIFIED; new_jpg_image.ct = UHDR_CT_UNSPECIFIED; new_jpg_image.range = UHDR_CR_UNSPECIFIED; std::unique_ptr dest_data; if (decoder.getEXIFPos() >= 0) { if (pExif != nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "received exif from uhdr_enc_set_exif_data() while the base image intent already " "contains exif, unsure which one to use"); return status; } copyJpegWithoutExif(&new_jpg_image, sdr_intent_compressed, decoder.getEXIFPos(), decoder.getEXIFSize()); dest_data.reset(reinterpret_cast(new_jpg_image.data)); exif_from_jpg.data = decoder.getEXIFPtr(); exif_from_jpg.data_sz = decoder.getEXIFSize(); pExif = &exif_from_jpg; } uhdr_compressed_image_t* final_primary_jpg_image_ptr = new_jpg_image.data_sz == 0 ? sdr_intent_compressed : &new_jpg_image; size_t pos = 0; // Begin primary image // Write SOI UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos)); // Write EXIF if (pExif != nullptr) { const size_t length = 2 + pExif->data_sz; const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, pExif->data, pExif->data_sz, pos)); } // Prepare and write XMP if (kWriteXmpMetadata) { const string xmp_primary = generateXmpForPrimaryImage(secondary_image_size, *metadata); const size_t length = 2 + xmpNameSpaceLength + xmp_primary.size(); const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, (void*)kXmpNameSpace.c_str(), xmpNameSpaceLength, pos)); UHDR_ERR_CHECK(Write(dest, (void*)xmp_primary.c_str(), xmp_primary.size(), pos)); } // Write ICC if (pIcc != nullptr && icc_size > 0) { const size_t length = icc_size + 2; const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, pIcc, icc_size, pos)); } // Prepare and write ISO 21496-1 metadata if (kWriteIso21496_1Metadata) { const size_t length = 2 + isoNameSpaceLength + 4; uint8_t zero = 0; const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, (void*)kIsoNameSpace.c_str(), isoNameSpaceLength, pos)); UHDR_ERR_CHECK(Write(dest, &zero, 1, pos)); UHDR_ERR_CHECK(Write(dest, &zero, 1, pos)); // 2 bytes minimum_version: (00 00) UHDR_ERR_CHECK(Write(dest, &zero, 1, pos)); UHDR_ERR_CHECK(Write(dest, &zero, 1, pos)); // 2 bytes writer_version: (00 00) } // Prepare and write MPF { const size_t length = 2 + calculateMpfSize(); const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); size_t primary_image_size = pos + length + final_primary_jpg_image_ptr->data_sz; // between APP2 + package size + signature // ff e2 00 58 4d 50 46 00 // 2 + 2 + 4 = 8 (bytes) // and ff d8 sign of the secondary image size_t secondary_image_offset = primary_image_size - pos - 8; std::shared_ptr mpf = generateMpf(primary_image_size, 0, /* primary_image_offset */ secondary_image_size, secondary_image_offset); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, (void*)mpf->getData(), mpf->getLength(), pos)); } // Write primary image UHDR_ERR_CHECK(Write(dest, (uint8_t*)final_primary_jpg_image_ptr->data + 2, final_primary_jpg_image_ptr->data_sz - 2, pos)); // Finish primary image // Begin secondary image (gain map) // Write SOI UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos)); // Prepare and write XMP if (kWriteXmpMetadata) { const size_t length = xmp_secondary_length; const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, (void*)kXmpNameSpace.c_str(), xmpNameSpaceLength, pos)); UHDR_ERR_CHECK(Write(dest, (void*)xmp_secondary.c_str(), xmp_secondary.size(), pos)); } // Prepare and write ISO 21496-1 metadata if (kWriteIso21496_1Metadata) { const size_t length = iso_secondary_length; const uint8_t lengthH = ((length >> 8) & 0xff); const uint8_t lengthL = (length & 0xff); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos)); UHDR_ERR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthH, 1, pos)); UHDR_ERR_CHECK(Write(dest, &lengthL, 1, pos)); UHDR_ERR_CHECK(Write(dest, (void*)kIsoNameSpace.c_str(), isoNameSpaceLength, pos)); UHDR_ERR_CHECK(Write(dest, (void*)iso_secondary_data.data(), iso_secondary_data.size(), pos)); } // Write secondary image UHDR_ERR_CHECK( Write(dest, (uint8_t*)gainmap_compressed->data + 2, gainmap_compressed->data_sz - 2, pos)); // Set back length dest->data_sz = pos; // Done! return g_no_error; } uhdr_error_info_t JpegR::getJPEGRInfo(uhdr_compressed_image_t* uhdr_compressed_img, jr_info_ptr uhdr_image_info) { uhdr_compressed_image_t primary_image, gainmap; UHDR_ERR_CHECK(extractPrimaryImageAndGainMap(uhdr_compressed_img, &primary_image, &gainmap)) UHDR_ERR_CHECK(parseJpegInfo(&primary_image, uhdr_image_info->primaryImgInfo, &uhdr_image_info->width, &uhdr_image_info->height)) if (uhdr_image_info->gainmapImgInfo != nullptr) { UHDR_ERR_CHECK(parseJpegInfo(&gainmap, uhdr_image_info->gainmapImgInfo)) } return g_no_error; } uhdr_error_info_t JpegR::parseGainMapMetadata(uint8_t* iso_data, size_t iso_size, uint8_t* xmp_data, size_t xmp_size, uhdr_gainmap_metadata_ext_t* uhdr_metadata) { if (iso_size > 0) { if (iso_size < kIsoNameSpace.size() + 1) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_ERROR; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "iso block size needs to be atleast %zd but got %zd", kIsoNameSpace.size() + 1, iso_size); return status; } uhdr_gainmap_metadata_frac decodedMetadata; std::vector iso_vec; for (size_t i = kIsoNameSpace.size() + 1; i < iso_size; i++) { iso_vec.push_back(iso_data[i]); } UHDR_ERR_CHECK(uhdr_gainmap_metadata_frac::decodeGainmapMetadata(iso_vec, &decodedMetadata)); UHDR_ERR_CHECK(uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&decodedMetadata, uhdr_metadata)); } else if (xmp_size > 0) { UHDR_ERR_CHECK(getMetadataFromXMP(xmp_data, xmp_size, uhdr_metadata)); } else { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "received no valid buffer to parse gainmap metadata"); return status; } return g_no_error; } /* Decode API */ uhdr_error_info_t JpegR::decodeJPEGR(uhdr_compressed_image_t* uhdr_compressed_img, uhdr_raw_image_t* dest, float max_display_boost, uhdr_color_transfer_t output_ct, uhdr_img_fmt_t output_format, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_t* gainmap_metadata) { uhdr_compressed_image_t primary_jpeg_image, gainmap_jpeg_image; UHDR_ERR_CHECK( extractPrimaryImageAndGainMap(uhdr_compressed_img, &primary_jpeg_image, &gainmap_jpeg_image)) JpegDecoderHelper jpeg_dec_obj_sdr; UHDR_ERR_CHECK(jpeg_dec_obj_sdr.decompressImage( primary_jpeg_image.data, primary_jpeg_image.data_sz, (output_ct == UHDR_CT_SRGB) ? DECODE_TO_RGB_CS : DECODE_TO_YCBCR_CS)); JpegDecoderHelper jpeg_dec_obj_gm; uhdr_raw_image_t gainmap; if (gainmap_img != nullptr || output_ct != UHDR_CT_SRGB) { UHDR_ERR_CHECK(jpeg_dec_obj_gm.decompressImage(gainmap_jpeg_image.data, gainmap_jpeg_image.data_sz, DECODE_STREAM)); gainmap = jpeg_dec_obj_gm.getDecompressedImage(); if (gainmap_img != nullptr) { UHDR_ERR_CHECK(copy_raw_image(&gainmap, gainmap_img)); } } uhdr_gainmap_metadata_ext_t uhdr_metadata; if (gainmap_metadata != nullptr || output_ct != UHDR_CT_SRGB) { UHDR_ERR_CHECK(parseGainMapMetadata(static_cast(jpeg_dec_obj_gm.getIsoMetadataPtr()), jpeg_dec_obj_gm.getIsoMetadataSize(), static_cast(jpeg_dec_obj_gm.getXMPPtr()), jpeg_dec_obj_gm.getXMPSize(), &uhdr_metadata)) if (gainmap_metadata != nullptr) { gainmap_metadata->min_content_boost = uhdr_metadata.min_content_boost; gainmap_metadata->max_content_boost = uhdr_metadata.max_content_boost; gainmap_metadata->gamma = uhdr_metadata.gamma; gainmap_metadata->offset_sdr = uhdr_metadata.offset_sdr; gainmap_metadata->offset_hdr = uhdr_metadata.offset_hdr; gainmap_metadata->hdr_capacity_min = uhdr_metadata.hdr_capacity_min; gainmap_metadata->hdr_capacity_max = uhdr_metadata.hdr_capacity_max; } } uhdr_raw_image_t sdr_intent = jpeg_dec_obj_sdr.getDecompressedImage(); sdr_intent.cg = IccHelper::readIccColorGamut(jpeg_dec_obj_sdr.getICCPtr(), jpeg_dec_obj_sdr.getICCSize()); if (output_ct == UHDR_CT_SRGB) { UHDR_ERR_CHECK(copy_raw_image(&sdr_intent, dest)); return g_no_error; } UHDR_ERR_CHECK(applyGainMap(&sdr_intent, &gainmap, &uhdr_metadata, output_ct, output_format, max_display_boost, dest)); return g_no_error; } uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* gainmap_metadata, uhdr_color_transfer_t output_ct, [[maybe_unused]] uhdr_img_fmt_t output_format, float max_display_boost, uhdr_raw_image_t* dest) { if (gainmap_metadata->version.compare(kJpegrVersion)) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "Unsupported gainmap metadata, version. Expected %s, Got %s", kJpegrVersion, gainmap_metadata->version.c_str()); return status; } UHDR_ERR_CHECK(uhdr_validate_gainmap_metadata_descriptor(gainmap_metadata)); if (sdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCr444 && sdr_intent->fmt != UHDR_IMG_FMT_16bppYCbCr422 && sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "apply gainmap method expects base image color format to be one of " "{UHDR_IMG_FMT_24bppYCbCr444, UHDR_IMG_FMT_16bppYCbCr422, " "UHDR_IMG_FMT_12bppYCbCr420}. Received %d", sdr_intent->fmt); return status; } if (gainmap_img->fmt != UHDR_IMG_FMT_8bppYCbCr400 && gainmap_img->fmt != UHDR_IMG_FMT_24bppRGB888 && gainmap_img->fmt != UHDR_IMG_FMT_32bppRGBA8888) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "apply gainmap method expects gainmap image color format to be one of " "{UHDR_IMG_FMT_8bppYCbCr400, UHDR_IMG_FMT_24bppRGB888, UHDR_IMG_FMT_32bppRGBA8888}. " "Received %d", gainmap_img->fmt); return status; } #ifdef UHDR_ENABLE_GLES if (mUhdrGLESCtxt != nullptr) { if (((sdr_intent->fmt == UHDR_IMG_FMT_12bppYCbCr420 && sdr_intent->w % 2 == 0 && sdr_intent->h % 2 == 0) || (sdr_intent->fmt == UHDR_IMG_FMT_16bppYCbCr422 && sdr_intent->w % 2 == 0) || (sdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCr444)) && isBufferDataContiguous(sdr_intent) && isBufferDataContiguous(gainmap_img) && isBufferDataContiguous(dest)) { // TODO: both inputs and outputs of GLES implementation assumes that raw image is contiguous // and without strides. If not, handle the same by using temp copy float display_boost = (std::min)(max_display_boost, gainmap_metadata->hdr_capacity_max); return applyGainMapGLES(sdr_intent, gainmap_img, gainmap_metadata, output_ct, display_boost, dest, static_cast(mUhdrGLESCtxt)); } } #endif std::unique_ptr resized_gainmap = nullptr; { float primary_aspect_ratio = (float)sdr_intent->w / sdr_intent->h; float gainmap_aspect_ratio = (float)gainmap_img->w / gainmap_img->h; float delta_aspect_ratio = fabs(primary_aspect_ratio - gainmap_aspect_ratio); // Allow 1% delta const float delta_tolerance = 0.01; if (delta_aspect_ratio / primary_aspect_ratio > delta_tolerance) { resized_gainmap = resize_image(gainmap_img, sdr_intent->w, sdr_intent->h); if (resized_gainmap == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "encountered error while resizing the gainmap image from %ux%u to %ux%u", gainmap_img->w, gainmap_img->h, sdr_intent->w, sdr_intent->h); return status; } gainmap_img = resized_gainmap.get(); } } float map_scale_factor = (float)sdr_intent->w / gainmap_img->w; int map_scale_factor_rnd = (std::max)(1, (int)std::roundf(map_scale_factor)); dest->cg = sdr_intent->cg; // Table will only be used when map scale factor is integer. ShepardsIDW idwTable(map_scale_factor_rnd); float display_boost = (std::min)(max_display_boost, gainmap_metadata->hdr_capacity_max); float gainmap_weight; if (display_boost != gainmap_metadata->hdr_capacity_max) { gainmap_weight = (log2(display_boost) - log2(gainmap_metadata->hdr_capacity_min)) / (log2(gainmap_metadata->hdr_capacity_max) - log2(gainmap_metadata->hdr_capacity_min)); // avoid extrapolating the gain map to fill the displayable range gainmap_weight = CLIP3(0.0f, gainmap_weight, 1.0f); } else { gainmap_weight = 1.0f; } GainLUT gainLUT(gainmap_metadata, gainmap_weight); GetPixelFn get_pixel_fn = getPixelFn(sdr_intent->fmt); if (get_pixel_fn == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for reading pixels for color format %d", sdr_intent->fmt); return status; } JobQueue jobQueue; std::function applyRecMap = [sdr_intent, gainmap_img, dest, &jobQueue, &idwTable, output_ct, &gainLUT, gainmap_metadata, #if !USE_APPLY_GAIN_LUT gainmap_weight, #endif map_scale_factor, get_pixel_fn]() -> void { unsigned int width = sdr_intent->w; unsigned int 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 = get_pixel_fn(sdr_intent, x, y); // Assuming the sdr image is a decoded JPEG, we should always use Rec.601 YUV coefficients Color rgb_gamma_sdr = p3YuvToRgb(yuv_gamma_sdr); // We are assuming the SDR base image is always sRGB transfer. #if USE_SRGB_INVOETF_LUT Color rgb_sdr = srgbInvOetfLUT(rgb_gamma_sdr); #else Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr); #endif Color rgb_hdr; if (gainmap_img->fmt == UHDR_IMG_FMT_8bppYCbCr400) { float gain; if (map_scale_factor != floorf(map_scale_factor)) { gain = sampleMap(gainmap_img, map_scale_factor, x, y); } else { gain = sampleMap(gainmap_img, map_scale_factor, x, y, idwTable); } #if USE_APPLY_GAIN_LUT rgb_hdr = applyGainLUT(rgb_sdr, gain, gainLUT, gainmap_metadata); #else rgb_hdr = applyGain(rgb_sdr, gain, gainmap_metadata, gainmap_weight); #endif } else { Color gain; if (map_scale_factor != floorf(map_scale_factor)) { gain = sampleMap3Channel(gainmap_img, map_scale_factor, x, y, gainmap_img->fmt == UHDR_IMG_FMT_32bppRGBA8888); } else { gain = sampleMap3Channel(gainmap_img, map_scale_factor, x, y, idwTable, gainmap_img->fmt == UHDR_IMG_FMT_32bppRGBA8888); } #if USE_APPLY_GAIN_LUT rgb_hdr = applyGainLUT(rgb_sdr, gain, gainLUT, gainmap_metadata); #else rgb_hdr = applyGain(rgb_sdr, gain, gainmap_metadata, gainmap_weight); #endif } size_t pixel_idx = x + y * dest->stride[UHDR_PLANE_PACKED]; switch (output_ct) { case UHDR_CT_LINEAR: { uint64_t rgba_f16 = colorToRgbaF16(rgb_hdr); reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx] = rgba_f16; break; } case UHDR_CT_HLG: { #if USE_HLG_OETF_LUT ColorTransformFn hdrOetf = hlgOetfLUT; #else ColorTransformFn hdrOetf = hlgOetf; #endif rgb_hdr = rgb_hdr * kSdrWhiteNits / kHlgMaxNits; rgb_hdr = hlgInverseOotfApprox(rgb_hdr); Color rgb_gamma_hdr = hdrOetf(rgb_hdr); uint32_t rgba_1010102 = colorToRgba1010102(rgb_gamma_hdr); reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx] = rgba_1010102; break; } case UHDR_CT_PQ: { #if USE_PQ_OETF_LUT ColorTransformFn hdrOetf = pqOetfLUT; #else ColorTransformFn hdrOetf = pqOetf; #endif rgb_hdr = rgb_hdr * kSdrWhiteNits / kPqMaxNits; Color rgb_gamma_hdr = hdrOetf(rgb_hdr); uint32_t rgba_1010102 = colorToRgba1010102(rgb_gamma_hdr); reinterpret_cast(dest->planes[UHDR_PLANE_PACKED])[pixel_idx] = rgba_1010102; break; } default: { } // Should be impossible to hit after input validation. } } } } }; const int threads = (std::min)(GetCPUCoreCount(), 4u); std::vector workers; for (int th = 0; th < threads - 1; th++) { workers.push_back(std::thread(applyRecMap)); } const unsigned int rowStep = threads == 1 ? sdr_intent->h : map_scale_factor_rnd; for (unsigned int rowStart = 0; rowStart < sdr_intent->h;) { unsigned int rowEnd = (std::min)(rowStart + rowStep, sdr_intent->h); jobQueue.enqueueJob(rowStart, rowEnd); rowStart = rowEnd; } jobQueue.markQueueForEnd(); applyRecMap(); std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); return g_no_error; } uhdr_error_info_t JpegR::extractPrimaryImageAndGainMap(uhdr_compressed_image_t* jpegr_image, uhdr_compressed_image_t* primary_image, uhdr_compressed_image_t* gainmap_image) { MessageHandler msg_handler; msg_handler.SetMessageWriter(make_unique(AlogMessageWriter())); std::shared_ptr seg = DataSegment::Create( DataRange(0, jpegr_image->data_sz), static_cast(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()) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_ERROR; status.has_detail = 1; auto messages = msg_handler.GetMessages(); std::string append{}; for (auto message : messages) append += message.GetText(); snprintf(status.detail, sizeof status.detail, "%s", append.c_str()); return status; } const auto& jpeg_info = jpeg_info_builder.GetInfo(); const auto& image_ranges = jpeg_info.GetImageRanges(); if (image_ranges.empty()) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "input uhdr image does not any valid images"); return status; } if (primary_image != nullptr) { primary_image->data = static_cast(jpegr_image->data) + image_ranges[0].GetBegin(); primary_image->data_sz = image_ranges[0].GetLength(); } if (image_ranges.size() == 1) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_INVALID_PARAM; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "input uhdr image does not contain gainmap image"); return status; } if (gainmap_image != nullptr) { gainmap_image->data = static_cast(jpegr_image->data) + image_ranges[1].GetBegin(); gainmap_image->data_sz = image_ranges[1].GetLength(); } // TODO: choose primary image and gain map image carefully if (image_ranges.size() > 2) { ALOGW("Number of jpeg images present %d, primary, gain map images may not be correctly chosen", (int)image_ranges.size()); } return g_no_error; } uhdr_error_info_t JpegR::parseJpegInfo(uhdr_compressed_image_t* jpeg_image, j_info_ptr image_info, unsigned int* img_width, unsigned int* img_height) { JpegDecoderHelper jpeg_dec_obj; UHDR_ERR_CHECK(jpeg_dec_obj.parseImage(jpeg_image->data, jpeg_image->data_sz)) unsigned int imgWidth, imgHeight, numComponents; imgWidth = jpeg_dec_obj.getDecompressedImageWidth(); imgHeight = jpeg_dec_obj.getDecompressedImageHeight(); numComponents = jpeg_dec_obj.getNumComponentsInImage(); if (image_info != nullptr) { image_info->width = imgWidth; image_info->height = imgHeight; image_info->numComponents = numComponents; image_info->imgData.resize(jpeg_image->data_sz, 0); memcpy(static_cast(image_info->imgData.data()), jpeg_image->data, jpeg_image->data_sz); if (jpeg_dec_obj.getICCSize() != 0) { image_info->iccData.resize(jpeg_dec_obj.getICCSize(), 0); memcpy(static_cast(image_info->iccData.data()), jpeg_dec_obj.getICCPtr(), jpeg_dec_obj.getICCSize()); } if (jpeg_dec_obj.getEXIFSize() != 0) { image_info->exifData.resize(jpeg_dec_obj.getEXIFSize(), 0); memcpy(static_cast(image_info->exifData.data()), jpeg_dec_obj.getEXIFPtr(), jpeg_dec_obj.getEXIFSize()); } if (jpeg_dec_obj.getXMPSize() != 0) { image_info->xmpData.resize(jpeg_dec_obj.getXMPSize(), 0); memcpy(static_cast(image_info->xmpData.data()), jpeg_dec_obj.getXMPPtr(), jpeg_dec_obj.getXMPSize()); } if (jpeg_dec_obj.getIsoMetadataSize() != 0) { image_info->isoData.resize(jpeg_dec_obj.getIsoMetadataSize(), 0); memcpy(static_cast(image_info->isoData.data()), jpeg_dec_obj.getIsoMetadataPtr(), jpeg_dec_obj.getIsoMetadataSize()); } } if (img_width != nullptr && img_height != nullptr) { *img_width = imgWidth; *img_height = imgHeight; } return g_no_error; } static float ReinhardMap(float y_hdr, float headroom) { float out = 1.0 + y_hdr / (headroom * headroom); out /= 1.0 + y_hdr; return out * y_hdr; } GlobalTonemapOutputs globalTonemap(const std::array& rgb_in, float headroom, bool is_normalized) { // Scale to Headroom to get HDR values that are referenced to SDR white. The range [0.0, 1.0] is // linearly stretched to [0.0, headroom]. std::array rgb_hdr; std::transform(rgb_in.begin(), rgb_in.end(), rgb_hdr.begin(), [&](float x) { return is_normalized ? x * headroom : x; }); // Apply a tone mapping to compress the range [0, headroom] to [0, 1] by // keeping the shadows the same and crushing the highlights. float max_hdr = *std::max_element(rgb_hdr.begin(), rgb_hdr.end()); float max_sdr = ReinhardMap(max_hdr, headroom); std::array rgb_sdr; std::transform(rgb_hdr.begin(), rgb_hdr.end(), rgb_sdr.begin(), [&](float x) { if (x > 0.0f) { return x * max_sdr / max_hdr; } return 0.0f; }); GlobalTonemapOutputs tonemap_outputs; tonemap_outputs.rgb_out = rgb_sdr; tonemap_outputs.y_hdr = max_hdr; tonemap_outputs.y_sdr = max_sdr; return tonemap_outputs; } uint8_t ScaleTo8Bit(float value) { constexpr float kMaxValFloat = 255.0f; constexpr int kMaxValInt = 255; return std::clamp(static_cast(std::round(value * kMaxValFloat)), 0, kMaxValInt); } uhdr_error_info_t JpegR::toneMap(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent) { if (hdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCrP010 && hdr_intent->fmt != UHDR_IMG_FMT_30bppYCbCr444 && hdr_intent->fmt != UHDR_IMG_FMT_32bppRGBA1010102 && hdr_intent->fmt != UHDR_IMG_FMT_64bppRGBAHalfFloat) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "tonemap method expects hdr intent color format to be one of " "{UHDR_IMG_FMT_24bppYCbCrP010, UHDR_IMG_FMT_30bppYCbCr444, " "UHDR_IMG_FMT_32bppRGBA1010102, UHDR_IMG_FMT_64bppRGBAHalfFloat}. Received %d", hdr_intent->fmt); return status; } if (hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010 && sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "tonemap method expects sdr intent color format to be UHDR_IMG_FMT_12bppYCbCr420, if " "hdr intent color format is UHDR_IMG_FMT_24bppYCbCrP010. Received %d", sdr_intent->fmt); return status; } if (hdr_intent->fmt == UHDR_IMG_FMT_30bppYCbCr444 && sdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCr444) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "tonemap method expects sdr intent color format to be UHDR_IMG_FMT_24bppYCbCr444, if " "hdr intent color format is UHDR_IMG_FMT_30bppYCbCr444. Received %d", sdr_intent->fmt); return status; } if ((hdr_intent->fmt == UHDR_IMG_FMT_32bppRGBA1010102 || hdr_intent->fmt == UHDR_IMG_FMT_64bppRGBAHalfFloat) && sdr_intent->fmt != UHDR_IMG_FMT_32bppRGBA8888) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "tonemap method expects sdr intent color format to be UHDR_IMG_FMT_32bppRGBA8888, if " "hdr intent color format is UHDR_IMG_FMT_32bppRGBA1010102 or " "UHDR_IMG_FMT_64bppRGBAHalfFloat. Received %d", sdr_intent->fmt); return status; } ColorTransformFn hdrYuvToRgbFn = getYuvToRgbFn(hdr_intent->cg); if (hdrYuvToRgbFn == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for converting yuv to rgb for color gamut %d", hdr_intent->cg); return status; } LuminanceFn hdrLuminanceFn = getLuminanceFn(hdr_intent->cg); if (hdrLuminanceFn == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for calculating luminance for color gamut %d", hdr_intent->cg); return status; } SceneToDisplayLuminanceFn hdrOotfFn = getOotfFn(hdr_intent->ct); if (hdrOotfFn == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for calculating Ootf for color transfer %d", hdr_intent->ct); return status; } ColorTransformFn hdrInvOetf = getInverseOetfFn(hdr_intent->ct); if (hdrInvOetf == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for converting transfer characteristics %d to linear", hdr_intent->ct); return status; } float hdr_white_nits = getReferenceDisplayPeakLuminanceInNits(hdr_intent->ct); if (hdr_white_nits == -1.0f) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "received invalid peak brightness %f nits for hdr reference display with color " "transfer %d ", hdr_white_nits, hdr_intent->ct); return status; } GetPixelFn get_pixel_fn = getPixelFn(hdr_intent->fmt); if (get_pixel_fn == nullptr) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for reading pixels for color format %d", hdr_intent->fmt); return status; } PutPixelFn put_pixel_fn = putPixelFn(sdr_intent->fmt); // for subsampled formats, we are writing to raw image buffers directly instead of using // put_pixel_fn if (put_pixel_fn == nullptr && sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420) { uhdr_error_info_t status; status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; snprintf(status.detail, sizeof status.detail, "No implementation available for writing pixels for color format %d", sdr_intent->fmt); return status; } sdr_intent->cg = UHDR_CG_DISPLAY_P3; sdr_intent->ct = UHDR_CT_SRGB; sdr_intent->range = UHDR_CR_FULL_RANGE; ColorTransformFn hdrGamutConversionFn = getGamutConversionFn(sdr_intent->cg, hdr_intent->cg); unsigned int height = hdr_intent->h; const int threads = (std::min)(GetCPUCoreCount(), 4u); // for 420 subsampling, process 2 rows at once const int jobSizeInRows = hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010 ? 2 : 1; unsigned int rowStep = threads == 1 ? height : jobSizeInRows; JobQueue jobQueue; std::function toneMapInternal; toneMapInternal = [hdr_intent, sdr_intent, hdrInvOetf, hdrGamutConversionFn, hdrYuvToRgbFn, hdr_white_nits, get_pixel_fn, put_pixel_fn, hdrLuminanceFn, hdrOotfFn, &jobQueue]() -> void { unsigned int rowStart, rowEnd; const int hfactor = hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010 ? 2 : 1; const int vfactor = hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010 ? 2 : 1; const bool isHdrIntentRgb = isPixelFormatRgb(hdr_intent->fmt); const bool isSdrIntentRgb = isPixelFormatRgb(sdr_intent->fmt); const bool is_normalized = hdr_intent->ct != UHDR_CT_LINEAR; uint8_t* luma_data = reinterpret_cast(sdr_intent->planes[UHDR_PLANE_Y]); uint8_t* cb_data = reinterpret_cast(sdr_intent->planes[UHDR_PLANE_U]); uint8_t* cr_data = reinterpret_cast(sdr_intent->planes[UHDR_PLANE_V]); size_t luma_stride = sdr_intent->stride[UHDR_PLANE_Y]; size_t cb_stride = sdr_intent->stride[UHDR_PLANE_U]; size_t cr_stride = sdr_intent->stride[UHDR_PLANE_V]; while (jobQueue.dequeueJob(rowStart, rowEnd)) { for (size_t y = rowStart; y < rowEnd; y += vfactor) { for (size_t x = 0; x < hdr_intent->w; x += hfactor) { // meant for p010 input float sdr_u_gamma = 0.0f; float sdr_v_gamma = 0.0f; for (int i = 0; i < vfactor; i++) { for (int j = 0; j < hfactor; j++) { Color hdr_rgb_gamma; if (isHdrIntentRgb) { hdr_rgb_gamma = get_pixel_fn(hdr_intent, x + j, y + i); } else { Color hdr_yuv_gamma = get_pixel_fn(hdr_intent, x + j, y + i); hdr_rgb_gamma = hdrYuvToRgbFn(hdr_yuv_gamma); } Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma); hdr_rgb = hdrOotfFn(hdr_rgb, hdrLuminanceFn); GlobalTonemapOutputs tonemap_outputs = globalTonemap( {hdr_rgb.r, hdr_rgb.g, hdr_rgb.b}, hdr_white_nits / kSdrWhiteNits, is_normalized); Color sdr_rgb_linear_bt2100 = { {{tonemap_outputs.rgb_out[0], tonemap_outputs.rgb_out[1], tonemap_outputs.rgb_out[2]}}}; Color sdr_rgb = hdrGamutConversionFn(sdr_rgb_linear_bt2100); // Hard clip out-of-gamut values; sdr_rgb = clampPixelFloat(sdr_rgb); Color sdr_rgb_gamma = srgbOetf(sdr_rgb); if (isSdrIntentRgb) { put_pixel_fn(sdr_intent, (x + j), (y + i), sdr_rgb_gamma); } else { Color sdr_yuv_gamma = p3RgbToYuv(sdr_rgb_gamma); sdr_yuv_gamma += {{{0.0f, 0.5f, 0.5f}}}; if (sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420) { put_pixel_fn(sdr_intent, (x + j), (y + i), sdr_yuv_gamma); } else { size_t out_y_idx = (y + i) * luma_stride + x + j; luma_data[out_y_idx] = ScaleTo8Bit(sdr_yuv_gamma.y); sdr_u_gamma += sdr_yuv_gamma.u; sdr_v_gamma += sdr_yuv_gamma.v; } } } } if (sdr_intent->fmt == UHDR_IMG_FMT_12bppYCbCr420) { sdr_u_gamma /= (hfactor * vfactor); sdr_v_gamma /= (hfactor * vfactor); cb_data[x / hfactor + (y / vfactor) * cb_stride] = ScaleTo8Bit(sdr_u_gamma); cr_data[x / hfactor + (y / vfactor) * cr_stride] = ScaleTo8Bit(sdr_v_gamma); } } } } }; // tone map std::vector workers; for (int th = 0; th < threads - 1; th++) { workers.push_back(std::thread(toneMapInternal)); } for (unsigned int rowStart = 0; rowStart < height;) { unsigned int rowEnd = (std::min)(rowStart + rowStep, height); jobQueue.enqueueJob(rowStart, rowEnd); rowStart = rowEnd; } jobQueue.markQueueForEnd(); toneMapInternal(); std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); }); return g_no_error; } status_t JpegR::areInputArgumentsValid(jr_uncompressed_ptr p010_image_ptr, jr_uncompressed_ptr yuv420_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest_ptr) { if (p010_image_ptr == nullptr || p010_image_ptr->data == nullptr) { ALOGE("Received nullptr for input p010 image"); return ERROR_JPEGR_BAD_PTR; } if (p010_image_ptr->width % 2 != 0 || p010_image_ptr->height % 2 != 0) { ALOGE("Image dimensions cannot be odd, image dimensions %ux%u", p010_image_ptr->width, p010_image_ptr->height); return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT; } if ((int)p010_image_ptr->width < kMinWidth || (int)p010_image_ptr->height < kMinHeight) { ALOGE("Image dimensions cannot be less than %dx%d, image dimensions %ux%u", kMinWidth, kMinHeight, p010_image_ptr->width, p010_image_ptr->height); return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT; } if ((int)p010_image_ptr->width > kMaxWidth || (int)p010_image_ptr->height > kMaxHeight) { ALOGE("Image dimensions cannot be larger than %dx%d, image dimensions %ux%u", kMaxWidth, kMaxHeight, p010_image_ptr->width, p010_image_ptr->height); return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT; } if (p010_image_ptr->colorGamut <= ULTRAHDR_COLORGAMUT_UNSPECIFIED || p010_image_ptr->colorGamut > ULTRAHDR_COLORGAMUT_MAX) { ALOGE("Unrecognized p010 color gamut %d", p010_image_ptr->colorGamut); return ERROR_JPEGR_INVALID_COLORGAMUT; } if (p010_image_ptr->luma_stride != 0 && p010_image_ptr->luma_stride < p010_image_ptr->width) { ALOGE("Luma stride must not be smaller than width, stride=%u, width=%u", p010_image_ptr->luma_stride, p010_image_ptr->width); return ERROR_JPEGR_INVALID_STRIDE; } if (p010_image_ptr->chroma_data != nullptr && p010_image_ptr->chroma_stride < p010_image_ptr->width) { ALOGE("Chroma stride must not be smaller than width, stride=%u, width=%u", p010_image_ptr->chroma_stride, p010_image_ptr->width); return ERROR_JPEGR_INVALID_STRIDE; } if (dest_ptr == nullptr || dest_ptr->data == nullptr) { ALOGE("Received nullptr for destination"); return ERROR_JPEGR_BAD_PTR; } if (hdr_tf <= ULTRAHDR_TF_UNSPECIFIED || hdr_tf > ULTRAHDR_TF_MAX || hdr_tf == ULTRAHDR_TF_SRGB) { ALOGE("Invalid hdr transfer function %d", hdr_tf); return ERROR_JPEGR_INVALID_TRANS_FUNC; } if (mMapDimensionScaleFactor <= 0 || mMapDimensionScaleFactor > 128) { ALOGE("gainmap scale factor is ecpected to be in range (0, 128], received %d", mMapDimensionScaleFactor); return ERROR_JPEGR_UNSUPPORTED_MAP_SCALE_FACTOR; } if (mMapCompressQuality < 0 || mMapCompressQuality > 100) { ALOGE("invalid quality factor %d, expects in range [0-100]", mMapCompressQuality); return ERROR_JPEGR_INVALID_QUALITY_FACTOR; } if (!std::isfinite(mGamma) || mGamma <= 0.0f) { ALOGE("unsupported gainmap gamma %f, expects to be > 0", mGamma); return ERROR_JPEGR_INVALID_GAMMA; } if (mEncPreset != UHDR_USAGE_REALTIME && mEncPreset != UHDR_USAGE_BEST_QUALITY) { ALOGE("invalid preset %d, expects one of {UHDR_USAGE_REALTIME, UHDR_USAGE_BEST_QUALITY}", mEncPreset); return ERROR_JPEGR_INVALID_ENC_PRESET; } if (!std::isfinite(mMinContentBoost) || !std::isfinite(mMaxContentBoost) || mMaxContentBoost < mMinContentBoost || mMinContentBoost <= 0.0f) { ALOGE("Invalid min boost / max boost configuration. Configured max boost %f, min boost %f", mMaxContentBoost, mMinContentBoost); return ERROR_JPEGR_INVALID_DISPLAY_BOOST; } if ((!std::isfinite(mTargetDispPeakBrightness) || mTargetDispPeakBrightness < ultrahdr::kSdrWhiteNits || mTargetDispPeakBrightness > ultrahdr::kPqMaxNits) && mTargetDispPeakBrightness != -1.0f) { ALOGE("unexpected target display peak brightness nits %f, expects to be with in range [%f %f]", mTargetDispPeakBrightness, ultrahdr::kSdrWhiteNits, ultrahdr::kPqMaxNits); return ERROR_JPEGR_INVALID_TARGET_DISP_PEAK_BRIGHTNESS; } if (yuv420_image_ptr == nullptr) { return JPEGR_NO_ERROR; } if (yuv420_image_ptr->data == nullptr) { ALOGE("Received nullptr for uncompressed 420 image"); return ERROR_JPEGR_BAD_PTR; } if (yuv420_image_ptr->luma_stride != 0 && yuv420_image_ptr->luma_stride < yuv420_image_ptr->width) { ALOGE("Luma stride must not be smaller than width, stride=%u, width=%u", yuv420_image_ptr->luma_stride, yuv420_image_ptr->width); return ERROR_JPEGR_INVALID_STRIDE; } if (yuv420_image_ptr->chroma_data != nullptr && yuv420_image_ptr->chroma_stride < yuv420_image_ptr->width / 2) { ALOGE("Chroma stride must not be smaller than (width / 2), stride=%u, width=%u", yuv420_image_ptr->chroma_stride, yuv420_image_ptr->width); return ERROR_JPEGR_INVALID_STRIDE; } if (p010_image_ptr->width != yuv420_image_ptr->width || p010_image_ptr->height != yuv420_image_ptr->height) { ALOGE("Image resolutions mismatch: P010: %ux%u, YUV420: %ux%u", p010_image_ptr->width, p010_image_ptr->height, yuv420_image_ptr->width, yuv420_image_ptr->height); return ERROR_JPEGR_RESOLUTION_MISMATCH; } if (yuv420_image_ptr->colorGamut <= ULTRAHDR_COLORGAMUT_UNSPECIFIED || yuv420_image_ptr->colorGamut > ULTRAHDR_COLORGAMUT_MAX) { ALOGE("Unrecognized 420 color gamut %d", yuv420_image_ptr->colorGamut); return ERROR_JPEGR_INVALID_COLORGAMUT; } return JPEGR_NO_ERROR; } status_t JpegR::areInputArgumentsValid(jr_uncompressed_ptr p010_image_ptr, jr_uncompressed_ptr yuv420_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest_ptr, int quality) { if (quality < 0 || quality > 100) { ALOGE("quality factor is out side range [0-100], quality factor : %d", quality); return ERROR_JPEGR_INVALID_QUALITY_FACTOR; } return areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest_ptr); } uhdr_color_transfer_t map_legacy_ct_to_ct(ultrahdr::ultrahdr_transfer_function ct) { switch (ct) { case ultrahdr::ULTRAHDR_TF_HLG: return UHDR_CT_HLG; case ultrahdr::ULTRAHDR_TF_PQ: return UHDR_CT_PQ; case ultrahdr::ULTRAHDR_TF_LINEAR: return UHDR_CT_LINEAR; case ultrahdr::ULTRAHDR_TF_SRGB: return UHDR_CT_SRGB; default: return UHDR_CT_UNSPECIFIED; } } uhdr_color_gamut_t map_legacy_cg_to_cg(ultrahdr::ultrahdr_color_gamut cg) { switch (cg) { case ultrahdr::ULTRAHDR_COLORGAMUT_BT2100: return UHDR_CG_BT_2100; case ultrahdr::ULTRAHDR_COLORGAMUT_BT709: return UHDR_CG_BT_709; case ultrahdr::ULTRAHDR_COLORGAMUT_P3: return UHDR_CG_DISPLAY_P3; default: return UHDR_CG_UNSPECIFIED; } } ultrahdr::ultrahdr_color_gamut map_cg_to_legacy_cg(uhdr_color_gamut_t cg) { switch (cg) { case UHDR_CG_BT_2100: return ultrahdr::ULTRAHDR_COLORGAMUT_BT2100; case UHDR_CG_BT_709: return ultrahdr::ULTRAHDR_COLORGAMUT_BT709; case UHDR_CG_DISPLAY_P3: return ultrahdr::ULTRAHDR_COLORGAMUT_P3; default: return ultrahdr::ULTRAHDR_COLORGAMUT_UNSPECIFIED; } } /* Encode API-0 */ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest, int quality, jr_exif_ptr exif) { // validate input arguments JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, nullptr, hdr_tf, dest, quality)); if (exif != nullptr && exif->data == nullptr) { ALOGE("received nullptr for exif metadata"); return ERROR_JPEGR_BAD_PTR; } // clean up input structure for later usage jpegr_uncompressed_struct p010_image = *p010_image_ptr; if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width; if (!p010_image.chroma_data) { uint16_t* data = reinterpret_cast(p010_image.data); p010_image.chroma_data = data + (size_t)p010_image.luma_stride * p010_image.height; p010_image.chroma_stride = p010_image.luma_stride; } uhdr_raw_image_t hdr_intent; hdr_intent.fmt = UHDR_IMG_FMT_24bppYCbCrP010; hdr_intent.cg = map_legacy_cg_to_cg(p010_image.colorGamut); hdr_intent.ct = map_legacy_ct_to_ct(hdr_tf); hdr_intent.range = p010_image.colorRange; hdr_intent.w = p010_image.width; hdr_intent.h = p010_image.height; hdr_intent.planes[UHDR_PLANE_Y] = p010_image.data; hdr_intent.stride[UHDR_PLANE_Y] = p010_image.luma_stride; hdr_intent.planes[UHDR_PLANE_UV] = p010_image.chroma_data; hdr_intent.stride[UHDR_PLANE_UV] = p010_image.chroma_stride; hdr_intent.planes[UHDR_PLANE_V] = nullptr; hdr_intent.stride[UHDR_PLANE_V] = 0; uhdr_compressed_image_t output; output.data = dest->data; output.data_sz = 0; output.capacity = dest->maxLength; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; uhdr_mem_block_t exifBlock; if (exif) { exifBlock.data = exif->data; exifBlock.data_sz = exifBlock.capacity = exif->length; } auto result = encodeJPEGR(&hdr_intent, &output, quality, exif ? &exifBlock : nullptr); if (result.error_code == UHDR_CODEC_OK) { dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->length = output.data_sz; } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } /* Encode API-1 */ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr, jr_uncompressed_ptr yuv420_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest, int quality, jr_exif_ptr exif) { // validate input arguments if (yuv420_image_ptr == nullptr) { ALOGE("received nullptr for uncompressed 420 image"); return ERROR_JPEGR_BAD_PTR; } if (exif != nullptr && exif->data == nullptr) { ALOGE("received nullptr for exif metadata"); return ERROR_JPEGR_BAD_PTR; } JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest, quality)) // clean up input structure for later usage jpegr_uncompressed_struct p010_image = *p010_image_ptr; if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width; if (!p010_image.chroma_data) { uint16_t* data = reinterpret_cast(p010_image.data); p010_image.chroma_data = data + (size_t)p010_image.luma_stride * p010_image.height; p010_image.chroma_stride = p010_image.luma_stride; } uhdr_raw_image_t hdr_intent; hdr_intent.fmt = UHDR_IMG_FMT_24bppYCbCrP010; hdr_intent.cg = map_legacy_cg_to_cg(p010_image.colorGamut); hdr_intent.ct = map_legacy_ct_to_ct(hdr_tf); hdr_intent.range = p010_image.colorRange; hdr_intent.w = p010_image.width; hdr_intent.h = p010_image.height; hdr_intent.planes[UHDR_PLANE_Y] = p010_image.data; hdr_intent.stride[UHDR_PLANE_Y] = p010_image.luma_stride; hdr_intent.planes[UHDR_PLANE_UV] = p010_image.chroma_data; hdr_intent.stride[UHDR_PLANE_UV] = p010_image.chroma_stride; hdr_intent.planes[UHDR_PLANE_V] = nullptr; hdr_intent.stride[UHDR_PLANE_V] = 0; jpegr_uncompressed_struct yuv420_image = *yuv420_image_ptr; if (yuv420_image.luma_stride == 0) yuv420_image.luma_stride = yuv420_image.width; if (!yuv420_image.chroma_data) { uint8_t* data = reinterpret_cast(yuv420_image.data); yuv420_image.chroma_data = data + (size_t)yuv420_image.luma_stride * yuv420_image.height; yuv420_image.chroma_stride = yuv420_image.luma_stride >> 1; } uhdr_raw_image_t sdrRawImg; sdrRawImg.fmt = UHDR_IMG_FMT_12bppYCbCr420; sdrRawImg.cg = map_legacy_cg_to_cg(yuv420_image.colorGamut); sdrRawImg.ct = UHDR_CT_SRGB; sdrRawImg.range = yuv420_image.colorRange; sdrRawImg.w = yuv420_image.width; sdrRawImg.h = yuv420_image.height; sdrRawImg.planes[UHDR_PLANE_Y] = yuv420_image.data; sdrRawImg.stride[UHDR_PLANE_Y] = yuv420_image.luma_stride; sdrRawImg.planes[UHDR_PLANE_U] = yuv420_image.chroma_data; sdrRawImg.stride[UHDR_PLANE_U] = yuv420_image.chroma_stride; uint8_t* data = reinterpret_cast(yuv420_image.chroma_data); data += (yuv420_image.height * yuv420_image.chroma_stride) / 2; sdrRawImg.planes[UHDR_PLANE_V] = data; sdrRawImg.stride[UHDR_PLANE_V] = yuv420_image.chroma_stride; auto sdr_intent = convert_raw_input_to_ycbcr(&sdrRawImg); uhdr_compressed_image_t output; output.data = dest->data; output.data_sz = 0; output.capacity = dest->maxLength; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; uhdr_mem_block_t exifBlock; if (exif) { exifBlock.data = exif->data; exifBlock.data_sz = exifBlock.capacity = exif->length; } auto result = encodeJPEGR(&hdr_intent, sdr_intent.get(), &output, quality, exif ? &exifBlock : nullptr); if (result.error_code == UHDR_CODEC_OK) { dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->length = output.data_sz; } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } /* Encode API-2 */ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr, jr_uncompressed_ptr yuv420_image_ptr, jr_compressed_ptr yuv420jpg_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest) { // validate input arguments if (yuv420_image_ptr == nullptr) { ALOGE("received nullptr for uncompressed 420 image"); return ERROR_JPEGR_BAD_PTR; } if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed jpeg image"); return ERROR_JPEGR_BAD_PTR; } JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest)) // clean up input structure for later usage jpegr_uncompressed_struct p010_image = *p010_image_ptr; if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width; if (!p010_image.chroma_data) { uint16_t* data = reinterpret_cast(p010_image.data); p010_image.chroma_data = data + (size_t)p010_image.luma_stride * p010_image.height; p010_image.chroma_stride = p010_image.luma_stride; } uhdr_raw_image_t hdr_intent; hdr_intent.fmt = UHDR_IMG_FMT_24bppYCbCrP010; hdr_intent.cg = map_legacy_cg_to_cg(p010_image.colorGamut); hdr_intent.ct = map_legacy_ct_to_ct(hdr_tf); hdr_intent.range = p010_image.colorRange; hdr_intent.w = p010_image.width; hdr_intent.h = p010_image.height; hdr_intent.planes[UHDR_PLANE_Y] = p010_image.data; hdr_intent.stride[UHDR_PLANE_Y] = p010_image.luma_stride; hdr_intent.planes[UHDR_PLANE_UV] = p010_image.chroma_data; hdr_intent.stride[UHDR_PLANE_UV] = p010_image.chroma_stride; hdr_intent.planes[UHDR_PLANE_V] = nullptr; hdr_intent.stride[UHDR_PLANE_V] = 0; jpegr_uncompressed_struct yuv420_image = *yuv420_image_ptr; if (yuv420_image.luma_stride == 0) yuv420_image.luma_stride = yuv420_image.width; if (!yuv420_image.chroma_data) { uint8_t* data = reinterpret_cast(yuv420_image.data); yuv420_image.chroma_data = data + (size_t)yuv420_image.luma_stride * p010_image.height; yuv420_image.chroma_stride = yuv420_image.luma_stride >> 1; } uhdr_raw_image_t sdrRawImg; sdrRawImg.fmt = UHDR_IMG_FMT_12bppYCbCr420; sdrRawImg.cg = map_legacy_cg_to_cg(yuv420_image.colorGamut); sdrRawImg.ct = UHDR_CT_SRGB; sdrRawImg.range = yuv420_image.colorRange; sdrRawImg.w = yuv420_image.width; sdrRawImg.h = yuv420_image.height; sdrRawImg.planes[UHDR_PLANE_Y] = yuv420_image.data; sdrRawImg.stride[UHDR_PLANE_Y] = yuv420_image.luma_stride; sdrRawImg.planes[UHDR_PLANE_U] = yuv420_image.chroma_data; sdrRawImg.stride[UHDR_PLANE_U] = yuv420_image.chroma_stride; uint8_t* data = reinterpret_cast(yuv420_image.chroma_data); data += (yuv420_image.height * yuv420_image.chroma_stride) / 2; sdrRawImg.planes[UHDR_PLANE_V] = data; sdrRawImg.stride[UHDR_PLANE_V] = yuv420_image.chroma_stride; auto sdr_intent = convert_raw_input_to_ycbcr(&sdrRawImg); uhdr_compressed_image_t input; input.data = yuv420jpg_image_ptr->data; input.data_sz = yuv420jpg_image_ptr->length; input.capacity = yuv420jpg_image_ptr->maxLength; input.cg = map_legacy_cg_to_cg(yuv420jpg_image_ptr->colorGamut); input.ct = UHDR_CT_UNSPECIFIED; input.range = UHDR_CR_UNSPECIFIED; uhdr_compressed_image_t output; output.data = dest->data; output.data_sz = 0; output.capacity = dest->maxLength; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; auto result = encodeJPEGR(&hdr_intent, sdr_intent.get(), &input, &output); if (result.error_code == UHDR_CODEC_OK) { dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->length = output.data_sz; } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } /* Encode API-3 */ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr, jr_compressed_ptr yuv420jpg_image_ptr, ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest) { // validate input arguments if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed jpeg image"); return ERROR_JPEGR_BAD_PTR; } JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, nullptr, hdr_tf, dest)) // clean up input structure for later usage jpegr_uncompressed_struct p010_image = *p010_image_ptr; if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width; if (!p010_image.chroma_data) { uint16_t* data = reinterpret_cast(p010_image.data); p010_image.chroma_data = data + (size_t)p010_image.luma_stride * p010_image.height; p010_image.chroma_stride = p010_image.luma_stride; } uhdr_raw_image_t hdr_intent; hdr_intent.fmt = UHDR_IMG_FMT_24bppYCbCrP010; hdr_intent.cg = map_legacy_cg_to_cg(p010_image.colorGamut); hdr_intent.ct = map_legacy_ct_to_ct(hdr_tf); hdr_intent.range = p010_image.colorRange; hdr_intent.w = p010_image.width; hdr_intent.h = p010_image.height; hdr_intent.planes[UHDR_PLANE_Y] = p010_image.data; hdr_intent.stride[UHDR_PLANE_Y] = p010_image.luma_stride; hdr_intent.planes[UHDR_PLANE_UV] = p010_image.chroma_data; hdr_intent.stride[UHDR_PLANE_UV] = p010_image.chroma_stride; hdr_intent.planes[UHDR_PLANE_V] = nullptr; hdr_intent.stride[UHDR_PLANE_V] = 0; uhdr_compressed_image_t input; input.data = yuv420jpg_image_ptr->data; input.data_sz = yuv420jpg_image_ptr->length; input.capacity = yuv420jpg_image_ptr->maxLength; input.cg = map_legacy_cg_to_cg(yuv420jpg_image_ptr->colorGamut); input.ct = UHDR_CT_UNSPECIFIED; input.range = UHDR_CR_UNSPECIFIED; uhdr_compressed_image_t output; output.data = dest->data; output.data_sz = 0; output.capacity = dest->maxLength; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; auto result = encodeJPEGR(&hdr_intent, &input, &output); if (result.error_code == UHDR_CODEC_OK) { dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->length = output.data_sz; } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } /* Encode API-4 */ status_t JpegR::encodeJPEGR(jr_compressed_ptr yuv420jpg_image_ptr, jr_compressed_ptr gainmapjpg_image_ptr, ultrahdr_metadata_ptr metadata, jr_compressed_ptr dest) { if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed jpeg image"); return ERROR_JPEGR_BAD_PTR; } if (gainmapjpg_image_ptr == nullptr || gainmapjpg_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed gain map"); return ERROR_JPEGR_BAD_PTR; } if (dest == nullptr || dest->data == nullptr) { ALOGE("received nullptr for destination"); return ERROR_JPEGR_BAD_PTR; } uhdr_compressed_image_t input; input.data = yuv420jpg_image_ptr->data; input.data_sz = yuv420jpg_image_ptr->length; input.capacity = yuv420jpg_image_ptr->maxLength; input.cg = map_legacy_cg_to_cg(yuv420jpg_image_ptr->colorGamut); input.ct = UHDR_CT_UNSPECIFIED; input.range = UHDR_CR_UNSPECIFIED; uhdr_compressed_image_t gainmap; gainmap.data = gainmapjpg_image_ptr->data; gainmap.data_sz = gainmapjpg_image_ptr->length; gainmap.capacity = gainmapjpg_image_ptr->maxLength; gainmap.cg = UHDR_CG_UNSPECIFIED; gainmap.ct = UHDR_CT_UNSPECIFIED; gainmap.range = UHDR_CR_UNSPECIFIED; uhdr_compressed_image_t output; output.data = dest->data; output.data_sz = 0; output.capacity = dest->maxLength; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; uhdr_gainmap_metadata_ext_t meta; meta.version = metadata->version; meta.hdr_capacity_max = metadata->hdrCapacityMax; meta.hdr_capacity_min = metadata->hdrCapacityMin; meta.gamma = metadata->gamma; meta.offset_sdr = metadata->offsetSdr; meta.offset_hdr = metadata->offsetHdr; meta.max_content_boost = metadata->maxContentBoost; meta.min_content_boost = metadata->minContentBoost; auto result = encodeJPEGR(&input, &gainmap, &meta, &output); if (result.error_code == UHDR_CODEC_OK) { dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->length = output.data_sz; } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } /* Decode API */ status_t JpegR::getJPEGRInfo(jr_compressed_ptr jpegr_image_ptr, jr_info_ptr jpegr_image_info_ptr) { if (jpegr_image_ptr == nullptr || jpegr_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed jpegr image"); return ERROR_JPEGR_BAD_PTR; } if (jpegr_image_info_ptr == nullptr) { ALOGE("received nullptr for compressed jpegr info struct"); return ERROR_JPEGR_BAD_PTR; } uhdr_compressed_image_t input; input.data = jpegr_image_ptr->data; input.data_sz = jpegr_image_ptr->length; input.capacity = jpegr_image_ptr->maxLength; input.cg = map_legacy_cg_to_cg(jpegr_image_ptr->colorGamut); input.ct = UHDR_CT_UNSPECIFIED; input.range = UHDR_CR_UNSPECIFIED; auto result = getJPEGRInfo(&input, jpegr_image_info_ptr); return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } status_t JpegR::decodeJPEGR(jr_compressed_ptr jpegr_image_ptr, jr_uncompressed_ptr dest, float max_display_boost, jr_exif_ptr exif, ultrahdr_output_format output_format, jr_uncompressed_ptr gainmap_image_ptr, ultrahdr_metadata_ptr metadata) { if (jpegr_image_ptr == nullptr || jpegr_image_ptr->data == nullptr) { ALOGE("received nullptr for compressed jpegr image"); return ERROR_JPEGR_BAD_PTR; } if (dest == nullptr || dest->data == nullptr) { ALOGE("received nullptr for dest image"); return ERROR_JPEGR_BAD_PTR; } if (max_display_boost < 1.0f) { ALOGE("received bad value for max_display_boost %f", max_display_boost); return ERROR_JPEGR_INVALID_DISPLAY_BOOST; } if (exif != nullptr && exif->data == nullptr) { ALOGE("received nullptr address for exif data"); return ERROR_JPEGR_BAD_PTR; } if (gainmap_image_ptr != nullptr && gainmap_image_ptr->data == nullptr) { ALOGE("received nullptr address for gainmap data"); return ERROR_JPEGR_BAD_PTR; } if (output_format <= ULTRAHDR_OUTPUT_UNSPECIFIED || output_format > ULTRAHDR_OUTPUT_MAX) { ALOGE("received bad value for output format %d", output_format); return ERROR_JPEGR_INVALID_OUTPUT_FORMAT; } uhdr_color_transfer_t ct; uhdr_img_fmt fmt; if (output_format == ULTRAHDR_OUTPUT_HDR_HLG) { fmt = UHDR_IMG_FMT_32bppRGBA1010102; ct = UHDR_CT_HLG; } else if (output_format == ULTRAHDR_OUTPUT_HDR_PQ) { fmt = UHDR_IMG_FMT_32bppRGBA1010102; ct = UHDR_CT_PQ; } else if (output_format == ULTRAHDR_OUTPUT_HDR_LINEAR) { fmt = UHDR_IMG_FMT_64bppRGBAHalfFloat; ct = UHDR_CT_LINEAR; } else if (output_format == ULTRAHDR_OUTPUT_SDR) { fmt = UHDR_IMG_FMT_32bppRGBA8888; ct = UHDR_CT_SRGB; } uhdr_compressed_image_t input; input.data = jpegr_image_ptr->data; input.data_sz = jpegr_image_ptr->length; input.capacity = jpegr_image_ptr->maxLength; input.cg = map_legacy_cg_to_cg(jpegr_image_ptr->colorGamut); input.ct = UHDR_CT_UNSPECIFIED; input.range = UHDR_CR_UNSPECIFIED; jpeg_info_struct primary_image; jpeg_info_struct gainmap_image; jpegr_info_struct jpegr_info; jpegr_info.primaryImgInfo = &primary_image; jpegr_info.gainmapImgInfo = &gainmap_image; if (getJPEGRInfo(&input, &jpegr_info).error_code != UHDR_CODEC_OK) return JPEGR_UNKNOWN_ERROR; if (exif != nullptr) { if (exif->length < primary_image.exifData.size()) { return ERROR_JPEGR_BUFFER_TOO_SMALL; } memcpy(exif->data, primary_image.exifData.data(), primary_image.exifData.size()); exif->length = primary_image.exifData.size(); } uhdr_raw_image_t output; output.fmt = fmt; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; output.w = jpegr_info.width; output.h = jpegr_info.height; output.planes[UHDR_PLANE_PACKED] = dest->data; output.stride[UHDR_PLANE_PACKED] = jpegr_info.width; output.planes[UHDR_PLANE_U] = nullptr; output.stride[UHDR_PLANE_U] = 0; output.planes[UHDR_PLANE_V] = nullptr; output.stride[UHDR_PLANE_V] = 0; uhdr_raw_image_t output_gm; if (gainmap_image_ptr) { output.fmt = gainmap_image.numComponents == 1 ? UHDR_IMG_FMT_8bppYCbCr400 : UHDR_IMG_FMT_24bppRGB888; output.cg = UHDR_CG_UNSPECIFIED; output.ct = UHDR_CT_UNSPECIFIED; output.range = UHDR_CR_UNSPECIFIED; output.w = gainmap_image.width; output.h = gainmap_image.height; output.planes[UHDR_PLANE_PACKED] = gainmap_image_ptr->data; output.stride[UHDR_PLANE_PACKED] = gainmap_image.width; output.planes[UHDR_PLANE_U] = nullptr; output.stride[UHDR_PLANE_U] = 0; output.planes[UHDR_PLANE_V] = nullptr; output.stride[UHDR_PLANE_V] = 0; } uhdr_gainmap_metadata_ext_t meta; auto result = decodeJPEGR(&input, &output, max_display_boost, ct, fmt, gainmap_image_ptr ? &output_gm : nullptr, metadata ? &meta : nullptr); if (result.error_code == UHDR_CODEC_OK) { dest->width = output.w; dest->height = output.h; dest->colorGamut = map_cg_to_legacy_cg(output.cg); dest->colorRange = output.range; dest->pixelFormat = output.fmt; dest->chroma_data = nullptr; if (gainmap_image_ptr) { gainmap_image_ptr->width = output_gm.w; gainmap_image_ptr->height = output_gm.h; gainmap_image_ptr->colorGamut = map_cg_to_legacy_cg(output_gm.cg); gainmap_image_ptr->colorRange = output_gm.range; gainmap_image_ptr->pixelFormat = output_gm.fmt; gainmap_image_ptr->chroma_data = nullptr; } if (metadata) { metadata->version = meta.version; metadata->hdrCapacityMax = meta.hdr_capacity_max; metadata->hdrCapacityMin = meta.hdr_capacity_min; metadata->gamma = meta.gamma; metadata->offsetSdr = meta.offset_sdr; metadata->offsetHdr = meta.offset_hdr; metadata->maxContentBoost = meta.max_content_boost; metadata->minContentBoost = meta.min_content_boost; } } return result.error_code == UHDR_CODEC_OK ? JPEGR_NO_ERROR : JPEGR_UNKNOWN_ERROR; } } // namespace ultrahdr