diff --git a/lib/include/ultrahdr/avifultrahdr.h b/lib/include/ultrahdr/avifultrahdr.h index f5b6f1bd..619a5e2e 100644 --- a/lib/include/ultrahdr/avifultrahdr.h +++ b/lib/include/ultrahdr/avifultrahdr.h @@ -71,6 +71,10 @@ class AvifUltraHdr : public UltraHdr { */ uhdr_error_info_t encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif); + // The owned-output variant resets dest on entry and leaves it empty on failure. + uhdr_error_info_t encodeAvifUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_owned_buffer_t* dest, int quality, + uhdr_mem_block_t* exif); /*!\brief Encode API-1. * @@ -93,6 +97,11 @@ class AvifUltraHdr : public UltraHdr { */ uhdr_error_info_t encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif); + // The owned-output variant resets dest on entry and leaves it empty on failure. + uhdr_error_info_t encodeAvifUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_raw_image_t* sdr_intent, + uhdr_owned_buffer_t* dest, int quality, + uhdr_mem_block_t* exif); /*!\brief Decode API. * @@ -153,7 +162,7 @@ class AvifUltraHdr : public UltraHdr { * 8-bit alpha in the base image * \param[in] gainmap_img gainmap raw image descriptor * \param[in] metadata gainmap metadata descriptor - * \param[in, out] dest output image descriptor to store compressed ultrahdr image + * \param[in, out] dest owned output buffer for compressed ultrahdr image * \param[in] quality quality factor for sdr intent heif/avif compression * \param[in] exif optional exif metadata that needs to be inserted in * compressed output @@ -166,7 +175,7 @@ class AvifUltraHdr : public UltraHdr { uhdr_raw_image_t* base_alpha_source, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* metadata, - uhdr_compressed_image_t* dest, int quality, + uhdr_owned_buffer_t* dest, int quality, uhdr_mem_block_t* exif, DataStruct* baseIcc, DataStruct* alternateIcc); diff --git a/lib/include/ultrahdr/heifultrahdr.h b/lib/include/ultrahdr/heifultrahdr.h index f7056554..e12580e3 100644 --- a/lib/include/ultrahdr/heifultrahdr.h +++ b/lib/include/ultrahdr/heifultrahdr.h @@ -71,6 +71,10 @@ class HeifUltraHdr : public UltraHdr { */ uhdr_error_info_t encodeHeicUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif); + // The owned-output variant resets dest on entry and leaves it empty on failure. + uhdr_error_info_t encodeHeicUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_owned_buffer_t* dest, int quality, + uhdr_mem_block_t* exif); /*!\brief Encode API-1. * @@ -93,6 +97,11 @@ class HeifUltraHdr : public UltraHdr { */ uhdr_error_info_t encodeHeicUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif); + // The owned-output variant resets dest on entry and leaves it empty on failure. + uhdr_error_info_t encodeHeicUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_raw_image_t* sdr_intent, + uhdr_owned_buffer_t* dest, int quality, + uhdr_mem_block_t* exif); /*!\brief Decode API. * @@ -153,7 +162,7 @@ class HeifUltraHdr : public UltraHdr { * 8-bit alpha in the base image * \param[in] gainmap_img gainmap raw image descriptor * \param[in] metadata gainmap metadata descriptor - * \param[in, out] dest output image descriptor to store compressed ultrahdr image + * \param[in, out] dest owned output buffer for compressed ultrahdr image * \param[in] quality quality factor for sdr intent heif/avif compression * \param[in] exif optional exif metadata that needs to be inserted in * compressed output @@ -166,7 +175,7 @@ class HeifUltraHdr : public UltraHdr { uhdr_raw_image_t* base_alpha_source, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* metadata, - uhdr_compressed_image_t* dest, int quality, + uhdr_owned_buffer_t* dest, int quality, uhdr_mem_block_t* exif, DataStruct* baseIcc, DataStruct* alternateIcc); diff --git a/lib/include/ultrahdr/ultrahdrcommon.h b/lib/include/ultrahdr/ultrahdrcommon.h index 6063781f..a6bcfc50 100644 --- a/lib/include/ultrahdr/ultrahdrcommon.h +++ b/lib/include/ultrahdr/ultrahdrcommon.h @@ -176,6 +176,30 @@ typedef struct uhdr_memory_block { size_t m_capacity; /**< capacity */ } uhdr_memory_block_t; /**< alias for struct uhdr_memory_block */ +/**\brief move-only buffer for transferring malloc-owned encoded output */ +typedef struct uhdr_owned_buffer { + uhdr_owned_buffer() noexcept; + ~uhdr_owned_buffer(); + + uhdr_owned_buffer(const uhdr_owned_buffer&) = delete; + uhdr_owned_buffer& operator=(const uhdr_owned_buffer&) = delete; + uhdr_owned_buffer(uhdr_owned_buffer&& other) noexcept; + uhdr_owned_buffer& operator=(uhdr_owned_buffer&& other) noexcept; + + /** Releases prior ownership and adopts malloc/free-compatible storage. */ + void reset(uint8_t* data = nullptr, size_t size = 0) noexcept; + uint8_t* data() const noexcept { return m_data; } + size_t size() const noexcept { return m_size; } + + private: + uint8_t* m_data; + size_t m_size; +} uhdr_owned_buffer_t; /**< alias for struct uhdr_owned_buffer */ + +uhdr_error_info_t invalidOutputDestination(); +uhdr_error_info_t copyOwnedBufferToCompressedImage(const uhdr_owned_buffer_t& source, + uhdr_compressed_image_t* dest); + /**\brief extended raw image descriptor */ typedef struct uhdr_raw_image_ext : uhdr_raw_image_t { uhdr_raw_image_ext(uhdr_img_fmt_t fmt, uhdr_color_gamut_t cg, uhdr_color_transfer_t ct, @@ -189,9 +213,12 @@ typedef struct uhdr_raw_image_ext : uhdr_raw_image_t { typedef struct uhdr_compressed_image_ext : uhdr_compressed_image_t { uhdr_compressed_image_ext(uhdr_color_gamut_t cg, uhdr_color_transfer_t ct, uhdr_color_range_t range, size_t sz); + uhdr_compressed_image_ext(uhdr_color_gamut_t cg, uhdr_color_transfer_t ct, + uhdr_color_range_t range, uhdr_owned_buffer_t&& buffer); private: std::unique_ptr m_block; + uhdr_owned_buffer_t m_owned_buffer; } uhdr_compressed_image_ext_t; /**< alias for struct uhdr_compressed_image_ext */ /*!\brief forward declaration for image effect descriptor */ diff --git a/lib/src/avifultrahdr.cpp b/lib/src/avifultrahdr.cpp index 779e95c1..0191ed5d 100644 --- a/lib/src/avifultrahdr.cpp +++ b/lib/src/avifultrahdr.cpp @@ -36,9 +36,15 @@ class MemoryWriter { ~MemoryWriter() { free(data_); } - const uint8_t* data() const { return data_; } + MemoryWriter(const MemoryWriter&) = delete; + MemoryWriter& operator=(const MemoryWriter&) = delete; - size_t size() const { return size_; } + void release(uhdr_owned_buffer_t* output) { + output->reset(data_, size_); + data_ = nullptr; + size_ = 0; + capacity_ = 0; + } struct heif_error write(const void* data, size_t size) { if (size == 0) return {heif_error_Ok, heif_suberror_Unspecified, nullptr}; @@ -343,6 +349,30 @@ AvifUltraHdr::AvifUltraHdr(void* uhdrGLESCtxt, int mapDimensionScaleFactor, int /* Encode API-0 */ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { + uhdr_owned_buffer_t output; + if (dest == nullptr || dest->data == nullptr) { + return invalidOutputDestination(); + } + uhdr_error_info_t status = encodeAvifUltraHdrToOwnedBuffer(hdr_intent, &output, quality, exif); + if (status.error_code != UHDR_CODEC_OK) return status; + return copyOwnedBufferToCompressedImage(output, dest); +} + +uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_owned_buffer_t* dest, + int quality, + uhdr_mem_block_t* exif) { + if (dest == nullptr) { + return invalidOutputDestination(); + } + dest->reset(); + if (hdr_intent == nullptr) { + uhdr_error_info_t status = g_no_error; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "hdr intent is null"); + return status; + } uhdr_img_fmt_t sdr_intent_fmt; if (hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010) { sdr_intent_fmt = UHDR_IMG_FMT_12bppYCbCr420; @@ -401,6 +431,30 @@ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { + uhdr_owned_buffer_t output; + if (dest == nullptr || dest->data == nullptr) { + return invalidOutputDestination(); + } + uhdr_error_info_t status = + encodeAvifUltraHdrToOwnedBuffer(hdr_intent, sdr_intent, &output, quality, exif); + if (status.error_code != UHDR_CODEC_OK) return status; + return copyOwnedBufferToCompressedImage(output, dest); +} + +uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdrToOwnedBuffer( + uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_owned_buffer_t* dest, + int quality, uhdr_mem_block_t* exif) { + if (dest == nullptr) { + return invalidOutputDestination(); + } + dest->reset(); + if (hdr_intent == nullptr || sdr_intent == nullptr) { + uhdr_error_info_t status = g_no_error; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "hdr or sdr intent is null"); + return status; + } // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; @@ -430,10 +484,13 @@ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* sdr_intent, uhdr_raw_image_t* base_alpha_source, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* metadata, - uhdr_compressed_image_t* dest, int quality, + uhdr_owned_buffer_t* dest, int quality, uhdr_mem_block_t* exif, DataStruct* baseIcc, DataStruct* alternateIcc) { uhdr_error_info_t status = g_no_error; + if (dest == nullptr) { + return invalidOutputDestination(); + } heif_encoder* encoder = nullptr; heif_encoding_options* options = nullptr; heif_color_profile_nclx* sdrNclx = nullptr; @@ -607,16 +664,7 @@ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* sdr_intent, snprintf(status.detail, sizeof status.detail, "%s", write_err.message); goto CleanUp; } - if (writer.size() > dest->capacity) { - status.error_code = UHDR_CODEC_MEM_ERROR; - status.has_detail = 1; - snprintf(status.detail, sizeof status.detail, - "destination buffer is too small, capacity is %zu, required size is %zu", - dest->capacity, writer.size()); - goto CleanUp; - } - memcpy(dest->data, writer.data(), writer.size()); - dest->data_sz = writer.size(); + writer.release(dest); CleanUp: if (baseImage) heif_image_release(baseImage); diff --git a/lib/src/heifultrahdr.cpp b/lib/src/heifultrahdr.cpp index 0bb9a421..e566f799 100644 --- a/lib/src/heifultrahdr.cpp +++ b/lib/src/heifultrahdr.cpp @@ -36,9 +36,15 @@ class MemoryWriter { ~MemoryWriter() { free(data_); } - const uint8_t* data() const { return data_; } + MemoryWriter(const MemoryWriter&) = delete; + MemoryWriter& operator=(const MemoryWriter&) = delete; - size_t size() const { return size_; } + void release(uhdr_owned_buffer_t* output) { + output->reset(data_, size_); + data_ = nullptr; + size_ = 0; + capacity_ = 0; + } struct heif_error write(const void* data, size_t size) { if (size == 0) return {heif_error_Ok, heif_suberror_Unspecified, nullptr}; @@ -343,6 +349,30 @@ HeifUltraHdr::HeifUltraHdr(void* uhdrGLESCtxt, int mapDimensionScaleFactor, int /* Encode API-0 */ uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { + uhdr_owned_buffer_t output; + if (dest == nullptr || dest->data == nullptr) { + return invalidOutputDestination(); + } + uhdr_error_info_t status = encodeHeicUltraHdrToOwnedBuffer(hdr_intent, &output, quality, exif); + if (status.error_code != UHDR_CODEC_OK) return status; + return copyOwnedBufferToCompressedImage(output, dest); +} + +uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdrToOwnedBuffer(uhdr_raw_image_t* hdr_intent, + uhdr_owned_buffer_t* dest, + int quality, + uhdr_mem_block_t* exif) { + if (dest == nullptr) { + return invalidOutputDestination(); + } + dest->reset(); + if (hdr_intent == nullptr) { + uhdr_error_info_t status = g_no_error; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "hdr intent is null"); + return status; + } uhdr_img_fmt_t sdr_intent_fmt; if (hdr_intent->fmt == UHDR_IMG_FMT_24bppYCbCrP010) { sdr_intent_fmt = UHDR_IMG_FMT_12bppYCbCr420; @@ -401,6 +431,30 @@ uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_compressed_image_t* dest, int quality, uhdr_mem_block_t* exif) { + uhdr_owned_buffer_t output; + if (dest == nullptr || dest->data == nullptr) { + return invalidOutputDestination(); + } + uhdr_error_info_t status = + encodeHeicUltraHdrToOwnedBuffer(hdr_intent, sdr_intent, &output, quality, exif); + if (status.error_code != UHDR_CODEC_OK) return status; + return copyOwnedBufferToCompressedImage(output, dest); +} + +uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdrToOwnedBuffer( + uhdr_raw_image_t* hdr_intent, uhdr_raw_image_t* sdr_intent, uhdr_owned_buffer_t* dest, + int quality, uhdr_mem_block_t* exif) { + if (dest == nullptr) { + return invalidOutputDestination(); + } + dest->reset(); + if (hdr_intent == nullptr || sdr_intent == nullptr) { + uhdr_error_info_t status = g_no_error; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "hdr or sdr intent is null"); + return status; + } // generate gain map uhdr_gainmap_metadata_ext_t metadata(kJpegrVersion); std::unique_ptr gainmap; @@ -430,10 +484,13 @@ uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdr(uhdr_raw_image_t* sdr_intent, uhdr_raw_image_t* base_alpha_source, uhdr_raw_image_t* gainmap_img, uhdr_gainmap_metadata_ext_t* metadata, - uhdr_compressed_image_t* dest, int quality, + uhdr_owned_buffer_t* dest, int quality, uhdr_mem_block_t* exif, DataStruct* baseIcc, DataStruct* alternateIcc) { uhdr_error_info_t status = g_no_error; + if (dest == nullptr) { + return invalidOutputDestination(); + } heif_encoder* encoder = nullptr; heif_encoding_options* options = nullptr; heif_color_profile_nclx* sdrNclx = nullptr; @@ -607,16 +664,7 @@ uhdr_error_info_t HeifUltraHdr::encodeHeicUltraHdr(uhdr_raw_image_t* sdr_intent, snprintf(status.detail, sizeof status.detail, "%s", write_err.message); goto CleanUp; } - if (writer.size() > dest->capacity) { - status.error_code = UHDR_CODEC_MEM_ERROR; - status.has_detail = 1; - snprintf(status.detail, sizeof status.detail, - "destination buffer is too small, capacity is %zu, required size is %zu", - dest->capacity, writer.size()); - goto CleanUp; - } - memcpy(dest->data, writer.data(), writer.size()); - dest->data_sz = writer.size(); + writer.release(dest); CleanUp: if (baseImage) heif_image_release(baseImage); diff --git a/lib/src/ultrahdr_api.cpp b/lib/src/ultrahdr_api.cpp index f5f6cb8e..b726eba3 100644 --- a/lib/src/ultrahdr_api.cpp +++ b/lib/src/ultrahdr_api.cpp @@ -9,7 +9,9 @@ */ #include +#include #include +#include #include "ultrahdr_api.h" #include "ultrahdr/ultrahdrcommon.h" @@ -42,6 +44,60 @@ using namespace photos_editing_formats::image_io; namespace ultrahdr { +uhdr_owned_buffer::uhdr_owned_buffer() noexcept : m_data(nullptr), m_size(0) {} + +uhdr_owned_buffer::~uhdr_owned_buffer() { free(m_data); } + +uhdr_owned_buffer::uhdr_owned_buffer(uhdr_owned_buffer&& other) noexcept + : m_data(other.m_data), m_size(other.m_size) { + other.m_data = nullptr; + other.m_size = 0; +} + +uhdr_owned_buffer& uhdr_owned_buffer::operator=(uhdr_owned_buffer&& other) noexcept { + if (this != &other) { + free(m_data); + m_data = other.m_data; + m_size = other.m_size; + other.m_data = nullptr; + other.m_size = 0; + } + return *this; +} + +void uhdr_owned_buffer::reset(uint8_t* data, size_t size) noexcept { + if (m_data != data) free(m_data); + m_data = data; + m_size = size; +} + +uhdr_error_info_t invalidOutputDestination() { + uhdr_error_info_t status = g_no_error; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "output destination is null"); + return status; +} + +uhdr_error_info_t copyOwnedBufferToCompressedImage(const uhdr_owned_buffer_t& source, + uhdr_compressed_image_t* dest) { + uhdr_error_info_t status = g_no_error; + if (dest == nullptr || dest->data == nullptr) { + return invalidOutputDestination(); + } + if (source.size() > dest->capacity) { + status.error_code = UHDR_CODEC_MEM_ERROR; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, + "destination buffer is too small, capacity is %zu, required size is %zu", + dest->capacity, source.size()); + return status; + } + if (source.size() != 0) memcpy(dest->data, source.data(), source.size()); + dest->data_sz = source.size(); + return status; +} + uhdr_memory_block::uhdr_memory_block(size_t capacity) { m_buffer = std::make_unique(capacity); m_capacity = capacity; @@ -130,6 +186,19 @@ uhdr_compressed_image_ext::uhdr_compressed_image_ext(uhdr_color_gamut_t cg_, this->range = range_; } +uhdr_compressed_image_ext::uhdr_compressed_image_ext(uhdr_color_gamut_t cg_, + uhdr_color_transfer_t ct_, + uhdr_color_range_t range_, + uhdr_owned_buffer_t&& buffer) + : m_owned_buffer(std::move(buffer)) { + this->data = this->m_owned_buffer.data(); + this->capacity = this->m_owned_buffer.size(); + this->data_sz = this->m_owned_buffer.size(); + this->cg = cg_; + this->ct = ct_; + this->range = range_; +} + uhdr_error_info_t apply_effects(uhdr_encoder_private* enc) { for (auto& it : enc->m_effects) { std::unique_ptr hdr_img = nullptr; @@ -1352,16 +1421,14 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { } else if (handle->m_raw_images.find(UHDR_HDR_IMG) != handle->m_raw_images.end()) { auto& hdr_raw_entry = handle->m_raw_images.find(UHDR_HDR_IMG)->second; - size_t size = (std::max)((64u * 1024), hdr_raw_entry->w * hdr_raw_entry->h * 3 * 2); - handle->m_compressed_output_buffer = std::make_unique( - UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, size); + ultrahdr::uhdr_owned_buffer_t output; if (handle->m_compressed_images.find(UHDR_SDR_IMG) == handle->m_compressed_images.end() && handle->m_raw_images.find(UHDR_SDR_IMG) == handle->m_raw_images.end()) { // api - 0 - status = avifultrahdr.encodeAvifUltraHdr(hdr_raw_entry.get(), handle->m_compressed_output_buffer.get(), - handle->m_quality.find(UHDR_BASE_IMG)->second, - handle->m_exif.size() > 0 ? &exif : nullptr); + status = avifultrahdr.encodeAvifUltraHdrToOwnedBuffer( + hdr_raw_entry.get(), &output, handle->m_quality.find(UHDR_BASE_IMG)->second, + handle->m_exif.size() > 0 ? &exif : nullptr); } else if (handle->m_compressed_images.find(UHDR_SDR_IMG) != handle->m_compressed_images.end() && handle->m_raw_images.find(UHDR_SDR_IMG) == handle->m_raw_images.end()) { @@ -1373,10 +1440,10 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { auto& sdr_raw_entry = handle->m_raw_images.find(UHDR_SDR_IMG)->second; if (handle->m_compressed_images.find(UHDR_SDR_IMG) == handle->m_compressed_images.end()) { // api - 1 - status = avifultrahdr.encodeAvifUltraHdr(hdr_raw_entry.get(), sdr_raw_entry.get(), - handle->m_compressed_output_buffer.get(), - handle->m_quality.find(UHDR_BASE_IMG)->second, - handle->m_exif.size() > 0 ? &exif : nullptr); + status = avifultrahdr.encodeAvifUltraHdrToOwnedBuffer( + hdr_raw_entry.get(), sdr_raw_entry.get(), &output, + handle->m_quality.find(UHDR_BASE_IMG)->second, + handle->m_exif.size() > 0 ? &exif : nullptr); } else { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; @@ -1384,6 +1451,12 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { "heif/avif encoding is supported only with raw intents"); } } + if (status.error_code == UHDR_CODEC_OK) { + handle->m_compressed_output_buffer = + std::make_unique( + UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, + std::move(output)); + } } } else if (handle->m_output_format == UHDR_CODEC_HEIF) { @@ -1401,16 +1474,14 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { } else if (handle->m_raw_images.find(UHDR_HDR_IMG) != handle->m_raw_images.end()) { auto& hdr_raw_entry = handle->m_raw_images.find(UHDR_HDR_IMG)->second; - size_t size = (std::max)((64u * 1024), hdr_raw_entry->w * hdr_raw_entry->h * 3 * 2); - handle->m_compressed_output_buffer = std::make_unique( - UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, size); + ultrahdr::uhdr_owned_buffer_t output; if (handle->m_compressed_images.find(UHDR_SDR_IMG) == handle->m_compressed_images.end() && handle->m_raw_images.find(UHDR_SDR_IMG) == handle->m_raw_images.end()) { // api - 0 - status = heifultrahdr.encodeHeicUltraHdr(hdr_raw_entry.get(), handle->m_compressed_output_buffer.get(), - handle->m_quality.find(UHDR_BASE_IMG)->second, - handle->m_exif.size() > 0 ? &exif : nullptr); + status = heifultrahdr.encodeHeicUltraHdrToOwnedBuffer( + hdr_raw_entry.get(), &output, handle->m_quality.find(UHDR_BASE_IMG)->second, + handle->m_exif.size() > 0 ? &exif : nullptr); } else if (handle->m_compressed_images.find(UHDR_SDR_IMG) != handle->m_compressed_images.end() && handle->m_raw_images.find(UHDR_SDR_IMG) == handle->m_raw_images.end()) { @@ -1422,10 +1493,10 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { auto& sdr_raw_entry = handle->m_raw_images.find(UHDR_SDR_IMG)->second; if (handle->m_compressed_images.find(UHDR_SDR_IMG) == handle->m_compressed_images.end()) { // api - 1 - status = heifultrahdr.encodeHeicUltraHdr(hdr_raw_entry.get(), sdr_raw_entry.get(), - handle->m_compressed_output_buffer.get(), - handle->m_quality.find(UHDR_BASE_IMG)->second, - handle->m_exif.size() > 0 ? &exif : nullptr); + status = heifultrahdr.encodeHeicUltraHdrToOwnedBuffer( + hdr_raw_entry.get(), sdr_raw_entry.get(), &output, + handle->m_quality.find(UHDR_BASE_IMG)->second, + handle->m_exif.size() > 0 ? &exif : nullptr); } else { status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE; status.has_detail = 1; @@ -1433,6 +1504,12 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { "heif/avif encoding is supported only with raw intents"); } } + if (status.error_code == UHDR_CODEC_OK) { + handle->m_compressed_output_buffer = + std::make_unique( + UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, + std::move(output)); + } } } #endif diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index f4b2edc9..cc08e04d 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -8,11 +8,13 @@ #include #endif #include +#include #include #include #include -#include #include +#include +#include #include #include "ultrahdr_api.h" @@ -517,6 +519,177 @@ bool encoderUnavailable(const uhdr_error_info_t& status) { } // namespace +using AvifApi0NullDestResult = decltype(std::declval().encodeAvifUltraHdr( + std::declval(), nullptr, 85, nullptr)); +using AvifApi1NullDestResult = decltype(std::declval().encodeAvifUltraHdr( + std::declval(), std::declval(), nullptr, 85, nullptr)); +using HeicApi0NullDestResult = decltype(std::declval().encodeHeicUltraHdr( + std::declval(), nullptr, 85, nullptr)); +using HeicApi1NullDestResult = decltype(std::declval().encodeHeicUltraHdr( + std::declval(), std::declval(), nullptr, 85, nullptr)); + +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +template +static void expectNullDataDestinationRejected(EncodeFn encode) { + uhdr_compressed_image_t dest{}; + dest.capacity = 32; + dest.data_sz = 9; + + uhdr_error_info_t status = encode(&dest); + EXPECT_EQ(status.error_code, UHDR_CODEC_INVALID_PARAM); + EXPECT_EQ(status.has_detail, 1); + EXPECT_STREQ(status.detail, "output destination is null"); + EXPECT_EQ(dest.data, nullptr); + EXPECT_EQ(dest.capacity, 32u); + EXPECT_EQ(dest.data_sz, 9u); +} + +TEST(UltraHdrOutputValidationTest, HeicApi0RejectsNullDataDestination) { + HeifUltraHdr codec; + expectNullDataDestinationRejected([&codec](uhdr_compressed_image_t* dest) { + return codec.encodeHeicUltraHdr(nullptr, dest, 85, nullptr); + }); +} + +TEST(UltraHdrOutputValidationTest, HeicApi1RejectsNullDataDestination) { + HeifUltraHdr codec; + expectNullDataDestinationRejected([&codec](uhdr_compressed_image_t* dest) { + return codec.encodeHeicUltraHdr(nullptr, nullptr, dest, 85, nullptr); + }); +} + +TEST(UltraHdrOutputValidationTest, AvifApi0RejectsNullDataDestination) { + AvifUltraHdr codec; + expectNullDataDestinationRejected([&codec](uhdr_compressed_image_t* dest) { + return codec.encodeAvifUltraHdr(nullptr, dest, 85, nullptr); + }); +} + +TEST(UltraHdrOutputValidationTest, AvifApi1RejectsNullDataDestination) { + AvifUltraHdr codec; + expectNullDataDestinationRejected([&codec](uhdr_compressed_image_t* dest) { + return codec.encodeAvifUltraHdr(nullptr, nullptr, dest, 85, nullptr); + }); +} + +static void expectLargeExifEncodeSucceeds(uhdr_codec_t codec) { + constexpr size_t kWidth = 64; + constexpr size_t kHeight = 64; + constexpr size_t kOldMinimumOutputCapacity = 64u * 1024u; + constexpr size_t kExifSize = 80u * 1024u; + + std::vector p010(kWidth * kHeight * 3 / 2, 512u << 6); + uhdr_raw_image_t hdr{}; + hdr.fmt = UHDR_IMG_FMT_24bppYCbCrP010; + hdr.cg = UHDR_CG_BT_2100; + hdr.ct = UHDR_CT_HLG; + hdr.range = UHDR_CR_FULL_RANGE; + hdr.w = kWidth; + hdr.h = kHeight; + hdr.planes[UHDR_PLANE_Y] = p010.data(); + hdr.planes[UHDR_PLANE_UV] = p010.data() + kWidth * kHeight; + hdr.stride[UHDR_PLANE_Y] = kWidth; + hdr.stride[UHDR_PLANE_UV] = kWidth; + + // Little-endian TIFF with one large UserComment entry. This makes the encoded output larger + // than the former 64 KiB minimum allocation without relying on encoder-specific image entropy. + std::vector exif(kExifSize, 0x5a); + const uint8_t tiff_header[] = { + 'I', 'I', 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, // TIFF header + 0x01, 0x00, // one IFD entry + 0x86, 0x92, // UserComment + 0x07, 0x00, // undefined data + 0xe6, 0x3f, 0x01, 0x00, // 81,894 bytes + 0x1a, 0x00, 0x00, 0x00, // data offset + 0x00, 0x00, 0x00, 0x00 // no next IFD + }; + memcpy(exif.data(), tiff_header, sizeof tiff_header); + uhdr_mem_block_t exif_block{exif.data(), exif.size(), exif.size()}; + + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + ASSERT_EQ(uhdr_enc_set_raw_image(enc, &hdr, UHDR_HDR_IMG).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, codec).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_exif_data(enc, &exif_block).error_code, UHDR_CODEC_OK); + + uhdr_error_info_t status = uhdr_encode(enc); + if (status.error_code != UHDR_CODEC_OK && status.has_detail && + (strstr(status.detail, "Unsupported file-type") != nullptr || + strstr(status.detail, "No encoder") != nullptr)) { + std::string detail = status.detail; + uhdr_release_encoder(enc); + GTEST_SKIP() << "encoder plugin not available in environment: " << detail; + } + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << (status.has_detail ? status.detail : ""); + + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + EXPECT_GT(output->data_sz, kOldMinimumOutputCapacity); + EXPECT_EQ(output->capacity, output->data_sz); + + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_decode(dec).error_code, UHDR_CODEC_OK); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); +} + +template +static void expectOwnedOutputResetsAcrossReuse(const uhdr_raw_image_t& hdr, EncodeFn encode) { + uhdr_owned_buffer_t output; + auto* seed = static_cast(malloc(1)); + ASSERT_NE(seed, nullptr); + output.reset(seed, 1); + + uhdr_raw_image_t valid_hdr = hdr; + uhdr_error_info_t status = encode(&valid_hdr, &output); + if (status.error_code != UHDR_CODEC_OK && status.has_detail && + (strstr(status.detail, "Unsupported file-type") != nullptr || + strstr(status.detail, "No encoder") != nullptr)) { + GTEST_SKIP() << "encoder plugin not available in environment: " << status.detail; + } + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << (status.has_detail ? status.detail : ""); + ASSERT_NE(output.data(), nullptr); + ASSERT_GT(output.size(), 0u); + + uhdr_raw_image_t invalid_hdr = hdr; + invalid_hdr.fmt = UHDR_IMG_FMT_UNSPECIFIED; + status = encode(&invalid_hdr, &output); + EXPECT_EQ(status.error_code, UHDR_CODEC_INVALID_PARAM); + EXPECT_EQ(output.data(), nullptr); + EXPECT_EQ(output.size(), 0u); +} + +template +static void expectFixedDestinationCopies(EncodeFn encode, const char* unavailable_encoder) { + std::vector backing_store(6 * kImageWidth * kImageHeight, 0xa5); + uhdr_compressed_image_t dest{}; + dest.data = backing_store.data(); + dest.capacity = backing_store.size(); + + uhdr_error_info_t status = encode(&dest); + if (status.error_code != UHDR_CODEC_OK && status.has_detail && + (strstr(status.detail, "Unsupported file-type") != nullptr || + strstr(status.detail, "No encoder") != nullptr)) { + GTEST_SKIP() << unavailable_encoder << ": " << status.detail; + } + + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << (status.has_detail ? status.detail : ""); + ASSERT_GT(dest.data_sz, 0u); + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + EXPECT_EQ(uhdr_dec_set_image(dec, &dest).error_code, UHDR_CODEC_OK); + EXPECT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_release_decoder(dec); +} + TEST_F(UltraHdrApiTest, HeicEncodeApi0AndDecode) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr); @@ -541,6 +714,7 @@ TEST_F(UltraHdrApiTest, HeicEncodeApi0AndDecode) { uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); + EXPECT_EQ(output->capacity, output->data_sz); // Decode HEIC stream uhdr_codec_private_t* dec = uhdr_create_decoder(); @@ -609,6 +783,7 @@ TEST_F(UltraHdrApiTest, HeicEncodeApi1AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); EXPECT_EQ(getPrimaryImageTransfer(output), heif_transfer_characteristic_IEC_61966_2_1); + EXPECT_EQ(output->capacity, output->data_sz); uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -620,11 +795,43 @@ TEST_F(UltraHdrApiTest, HeicEncodeApi1AndDecode) { uhdr_release_encoder(enc); } +TEST_F(UltraHdrApiTest, HeicEncodeSupportsOutputLargerThanSizeEstimate) { + expectLargeExifEncodeSucceeds(UHDR_CODEC_HEIF); +} + +TEST_F(UltraHdrApiTest, HeicFixedDestinationCopiesOutput) { + HeifUltraHdr codec; + expectFixedDestinationCopies( + [&codec, this](uhdr_compressed_image_t* dest) { + return codec.encodeHeicUltraHdr(&mHdrRaw, dest, 85, nullptr); + }, + "HEVC encoder plugin not available in environment"); +} + +TEST_F(UltraHdrApiTest, HeicApi1FixedDestinationCopiesOutput) { + HeifUltraHdr codec; + expectFixedDestinationCopies( + [&codec, this](uhdr_compressed_image_t* dest) { + return codec.encodeHeicUltraHdr(&mHdrRaw, &mSdrRaw, dest, 85, nullptr); + }, + "HEVC encoder plugin not available in environment"); +} + +TEST_F(UltraHdrApiTest, HeicOwnedOutputResetsAcrossReuse) { + HeifUltraHdr codec; + expectOwnedOutputResetsAcrossReuse( + mHdrRaw, [&codec](uhdr_raw_image_t* hdr, uhdr_owned_buffer_t* output) { + return codec.encodeHeicUltraHdrToOwnedBuffer(hdr, output, 85, nullptr); + }); +} + TEST_F(UltraHdrApiTest, HeicEncodeRejectsUndersizedDestination) { std::vector backing_store(6 * kImageWidth * kImageHeight, 0xa5); + const std::vector original = backing_store; uhdr_compressed_image_t dest{}; dest.data = backing_store.data(); dest.capacity = 1; + dest.data_sz = 7; HeifUltraHdr codec; uhdr_error_info_t status = codec.encodeHeicUltraHdr(&mHdrRaw, &dest, 85, nullptr); @@ -636,8 +843,11 @@ TEST_F(UltraHdrApiTest, HeicEncodeRejectsUndersizedDestination) { EXPECT_EQ(status.error_code, UHDR_CODEC_MEM_ERROR); EXPECT_EQ(dest.capacity, 1u); - EXPECT_EQ(dest.data_sz, 0u); - EXPECT_EQ(backing_store.front(), 0xa5); + EXPECT_EQ(dest.data_sz, 7u); + EXPECT_EQ(backing_store, original); + EXPECT_NE(strstr(status.detail, + "destination buffer is too small, capacity is 1, required size is "), + nullptr); } TEST_F(UltraHdrApiTest, HeicCompressedIntentsUnsupported) { @@ -682,6 +892,7 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi0AndDecode) { uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); + EXPECT_EQ(output->capacity, output->data_sz); // Decode AVIF stream uhdr_codec_private_t* dec = uhdr_create_decoder(); @@ -746,6 +957,7 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi1AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); EXPECT_EQ(getPrimaryImageTransfer(output), heif_transfer_characteristic_IEC_61966_2_1); + EXPECT_EQ(output->capacity, output->data_sz); uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -757,11 +969,43 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi1AndDecode) { uhdr_release_encoder(enc); } +TEST_F(UltraHdrApiTest, AvifEncodeSupportsOutputLargerThanSizeEstimate) { + expectLargeExifEncodeSucceeds(UHDR_CODEC_AVIF); +} + +TEST_F(UltraHdrApiTest, AvifFixedDestinationCopiesOutput) { + AvifUltraHdr codec; + expectFixedDestinationCopies( + [&codec, this](uhdr_compressed_image_t* dest) { + return codec.encodeAvifUltraHdr(&mHdrRaw, dest, 85, nullptr); + }, + "AV1 encoder plugin not available in environment"); +} + +TEST_F(UltraHdrApiTest, AvifApi1FixedDestinationCopiesOutput) { + AvifUltraHdr codec; + expectFixedDestinationCopies( + [&codec, this](uhdr_compressed_image_t* dest) { + return codec.encodeAvifUltraHdr(&mHdrRaw, &mSdrRaw, dest, 85, nullptr); + }, + "AV1 encoder plugin not available in environment"); +} + +TEST_F(UltraHdrApiTest, AvifOwnedOutputResetsAcrossReuse) { + AvifUltraHdr codec; + expectOwnedOutputResetsAcrossReuse( + mHdrRaw, [&codec](uhdr_raw_image_t* hdr, uhdr_owned_buffer_t* output) { + return codec.encodeAvifUltraHdrToOwnedBuffer(hdr, output, 85, nullptr); + }); +} + TEST_F(UltraHdrApiTest, AvifEncodeRejectsUndersizedDestination) { std::vector backing_store(6 * kImageWidth * kImageHeight, 0xa5); + const std::vector original = backing_store; uhdr_compressed_image_t dest{}; dest.data = backing_store.data(); dest.capacity = 1; + dest.data_sz = 7; AvifUltraHdr codec; uhdr_error_info_t status = codec.encodeAvifUltraHdr(&mHdrRaw, &dest, 85, nullptr); @@ -773,8 +1017,11 @@ TEST_F(UltraHdrApiTest, AvifEncodeRejectsUndersizedDestination) { EXPECT_EQ(status.error_code, UHDR_CODEC_MEM_ERROR); EXPECT_EQ(dest.capacity, 1u); - EXPECT_EQ(dest.data_sz, 0u); - EXPECT_EQ(backing_store.front(), 0xa5); + EXPECT_EQ(dest.data_sz, 7u); + EXPECT_EQ(backing_store, original); + EXPECT_NE(strstr(status.detail, + "destination buffer is too small, capacity is 1, required size is "), + nullptr); } TEST_F(UltraHdrApiTest, HeifAndAvifPropagateGainMapMetadataErrors) {