From a1de57a677407a24682d068d45db9035eb218539 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Tue, 22 Jul 2025 21:16:18 +1000 Subject: [PATCH] text: add extended language property support --- examples/heif_info.cc | 30 ++++++++++++++++ libheif/Doxyfile.in | 3 +- libheif/api/libheif/heif_properties.h | 3 +- libheif/api/libheif/heif_text.cc | 50 ++++++++++++++++++++++++++- libheif/api/libheif/heif_text.h | 31 ++++++++++++++++- libheif/box.cc | 32 +++++++++++++++++ libheif/box.h | 49 +++++++++++++++++++++++++- tests/text.cc | 19 ++++++++++ 8 files changed, 212 insertions(+), 5 deletions(-) diff --git a/examples/heif_info.cc b/examples/heif_info.cc index b45b4e35cf..0379c81f53 100644 --- a/examples/heif_info.cc +++ b/examples/heif_info.cc @@ -38,10 +38,12 @@ #endif #include +#include #include #include #include #include "libheif/heif_sequences.h" +#include #include #include @@ -639,6 +641,34 @@ int main(int argc, char** argv) printf(" none\n"); } + // --- text items + int numTextItems = heif_image_handle_get_number_of_text_items(handle); + printf("text items:\n"); + + if (numTextItems > 0) { + std::vector text_items(numTextItems); + heif_image_handle_get_list_of_text_item_ids(handle, text_items.data(), numTextItems); + for (heif_item_id text_item_id : text_items) { + struct heif_text_item* text_item; + err = heif_context_get_text_item(ctx.get(), text_item_id, &text_item); + const char* text_content = heif_text_item_get_content(text_item); + printf(" text item: %s\n", text_content); + heif_string_release(text_content); + + char* elng; + err = heif_item_get_property_extended_language(ctx.get(), + text_item_id, + &elng); + if (err.code == 0) { + printf(" extended language: %s\n", elng); + } + heif_string_release(elng); + } + } + else { + printf(" none\n"); + } + // --- properties printf("properties:\n"); diff --git a/libheif/Doxyfile.in b/libheif/Doxyfile.in index 52d7ff6e1a..894a8bbb54 100644 --- a/libheif/Doxyfile.in +++ b/libheif/Doxyfile.in @@ -858,7 +858,8 @@ WARN_LOGFILE = INPUT = @CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif.h \ @CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_items.h \ -@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_regions.h +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_regions.h \ +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_text.h # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/libheif/api/libheif/heif_properties.h b/libheif/api/libheif/heif_properties.h index 295c88ea23..9a3f15b55b 100644 --- a/libheif/api/libheif/heif_properties.h +++ b/libheif/api/libheif/heif_properties.h @@ -40,7 +40,8 @@ enum heif_item_property_type heif_item_property_type_image_size = heif_fourcc('i', 's', 'p', 'e'), heif_item_property_type_uuid = heif_fourcc('u', 'u', 'i', 'd'), heif_item_property_type_tai_clock_info = heif_fourcc('t', 'a', 'i', 'c'), - heif_item_property_type_tai_timestamp = heif_fourcc('i', 't', 'a', 'i') + heif_item_property_type_tai_timestamp = heif_fourcc('i', 't', 'a', 'i'), + heif_item_property_type_extended_language = heif_fourcc('e', 'l', 'n', 'g') }; // Get the heif_property_id for a heif_item_id. diff --git a/libheif/api/libheif/heif_text.cc b/libheif/api/libheif/heif_text.cc index 6cba056df1..fc24afbf1c 100644 --- a/libheif/api/libheif/heif_text.cc +++ b/libheif/api/libheif/heif_text.cc @@ -21,12 +21,14 @@ #include "heif_text.h" #include "api_structs.h" +#include "file.h" #include "text.h" #include #include #include -#include #include +#include +#include struct heif_error heif_image_handle_add_text_item(heif_image_handle *image_handle, const char *content_type, @@ -114,3 +116,49 @@ const char* heif_text_item_get_content(struct heif_text_item* text_item) return text_c; } + +struct heif_error heif_item_get_property_extended_language(const heif_context* context, + heif_item_id itemId, + char** out_language) +{ + if (!out_language || !context) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + } + + auto elng = context->context->find_property(itemId); + if (!elng) { + return elng.error_struct(context->context.get()); + } + + std::string lang = (*elng)->get_extended_language(); + *out_language = new char[lang.length() + 1]; + strcpy(*out_language, lang.c_str()); + + return heif_error_success; +} + +struct heif_error heif_text_item_set_extended_language(heif_text_item* text_item, const char *language, heif_property_id* out_optional_propertyId) +{ + if (!text_item || !language) { + return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + } + + if (auto img = text_item->context->get_image(text_item->text_item->get_item_id(), false)) { + auto existing_elng = img->get_property(); + if (existing_elng) { + existing_elng->set_lang(std::string(language)); + return heif_error_success; + } + } + + auto elng = std::make_shared(); + elng->set_lang(std::string(language)); + + heif_property_id id = text_item->context->add_property(text_item->text_item->get_item_id(), elng, false); + + if (out_optional_propertyId) { + *out_optional_propertyId = id; + } + + return heif_error_success; +} \ No newline at end of file diff --git a/libheif/api/libheif/heif_text.h b/libheif/api/libheif/heif_text.h index a1d99d15bd..e46e14879a 100644 --- a/libheif/api/libheif/heif_text.h +++ b/libheif/api/libheif/heif_text.h @@ -85,7 +85,7 @@ heif_error heif_context_get_text_item(const heif_context* context, /** * Get the item identifier for a text item. * - * @param region_item the text item to query + * @param text_item the text item to query * @return the text item identifier (or 0 if the text_item is null) */ LIBHEIF_API @@ -103,6 +103,19 @@ heif_item_id heif_text_item_get_id(heif_text_item* text_item); LIBHEIF_API const char* heif_text_item_get_content(heif_text_item* text_item); +/** + * Get the extended language associated with the text item. + * + * @param context the context to get the text item from, usually from a file operation + * @param itemId the identifier for the text item + * @param out_language pointer to pointer to the resulting language + * @return heif_error_ok on success, or an error value indicating the problem + */ +LIBHEIF_API +heif_error heif_item_get_property_extended_language(const heif_context* context, + heif_item_id itemId, + char** out_language); + // --- adding text items /** @@ -124,6 +137,22 @@ heif_error heif_image_handle_add_text_item(heif_image_handle *image_handle, LIBHEIF_API void heif_text_item_release(heif_text_item* text_item); +/** + * Set the extended language property to the text item. + * + * This adds an RFC 5346 (IETF BCP 47) extended language tag, such as "en-AU". + * + * @param text_item the text item to query + * @param language the language to set + * @param out_optional_propertyId Output parameter for the property ID of the language property. + * This parameter may be NULL if the info is not required. + * @return heif_error_ok on success, or an error value indicating the problem + */ +LIBHEIF_API +heif_error heif_text_item_set_extended_language(heif_text_item* text_item, + const char *language, + heif_property_id* out_optional_propertyId); + #ifdef __cplusplus } #endif diff --git a/libheif/box.cc b/libheif/box.cc index a474488c5b..f2b41cb9ce 100644 --- a/libheif/box.cc +++ b/libheif/box.cc @@ -646,6 +646,11 @@ Error Box::read(BitstreamRange& range, std::shared_ptr* result, const heif_ box = std::make_shared(); break; + case fourcc("elng"): + box = std::make_shared(); + break; + + #if WITH_UNCOMPRESSED_CODEC case fourcc("cmpd"): box = std::make_shared(); @@ -4990,3 +4995,30 @@ Error Box_itai::parse(BitstreamRange& range, const heif_security_limits*) { return range.get_error(); } +Error Box_elng::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("elng"); + } + + m_lang = range.read_string(); + return range.get_error(); +} + +std::string Box_elng::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + sstr << indent << "extended_language: " << m_lang << "\n"; + return sstr.str(); +} + +Error Box_elng::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + writer.write(m_lang); + prepend_header(writer, box_start); + return Error::Ok; +} diff --git a/libheif/box.h b/libheif/box.h index 297c8a32f3..564688e4cc 100644 --- a/libheif/box.h +++ b/libheif/box.h @@ -1818,4 +1818,51 @@ class Box_itai : public FullBox bool operator==(const heif_tai_timestamp_packet& a, const heif_tai_timestamp_packet& b); -#endif \ No newline at end of file + +/** + * Extended language property. + * + * Permits the association of language information with an item. + * + * See ISO/IEC 23008-12:2025(E) Section 6.10.2.2 and ISO/IEC 14496-12:2022(E) Section 8.4.6. + */ +class Box_elng : public FullBox +{ +public: + Box_elng() + { + set_short_type(fourcc("elng")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Extended language"; } + + Error write(StreamWriter& writer) const override; + + /** + * Language. + * + * An RFC 5646 (IETF BCP 47) compliant language identifier for the language of the text. + * Examples: "en-AU", "de-DE", or "zh-CN“. + */ + std::string get_extended_language() const { return m_lang; } + + /** + * Set the language. + * + * An RFC 5646 (IETF BCP 47) compliant language identifier for the language of the text. + * Examples: "en-AU", "de-DE", or "zh-CN“. + */ + void set_lang(const std::string lang) { m_lang = lang; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::string m_lang; +}; + +#endif diff --git a/tests/text.cc b/tests/text.cc index 8df27f5afb..edebbff1a7 100644 --- a/tests/text.cc +++ b/tests/text.cc @@ -33,6 +33,7 @@ #include #include #include +#include TEST_CASE("no text") { @@ -83,11 +84,17 @@ TEST_CASE("create text item") { std::string text_body1("first string"); err = heif_image_handle_add_text_item(handle, "text/plain", text_body1.c_str(), &text_item1); REQUIRE(err.code == heif_error_Ok); + err = heif_text_item_set_extended_language(text_item1, "en-AU", NULL); + REQUIRE(err.code == heif_error_Ok); struct heif_text_item* text_item2; std::string text_body2("a second string"); err = heif_image_handle_add_text_item(handle, "text/plain", text_body2.c_str(), &text_item2); REQUIRE(err.code == heif_error_Ok); + heif_property_id elng_prop_id; + err = heif_text_item_set_extended_language(text_item2, "en-UK", &elng_prop_id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(elng_prop_id != 0); err = heif_context_write_to_file(ctx, "text.heif"); REQUIRE(err.code == heif_error_Ok); @@ -128,6 +135,12 @@ TEST_CASE("create text item") { REQUIRE(std::string(content_type0) == "text/plain"); const char* body0 = heif_text_item_get_content(text0); REQUIRE(std::string(body0) == text_body1); + heif_string_release(body0); + char * elng0; + err = heif_item_get_property_extended_language(readbackCtx, id0, &elng0); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(strcmp(elng0, "en-AU") == 0); + heif_string_release(elng0); heif_text_item* text1; err = heif_context_get_text_item(readbackCtx, text_item_ids[1], &text1); @@ -139,6 +152,12 @@ TEST_CASE("create text item") { REQUIRE(std::string(content_type1) == "text/plain"); const char* body1 = heif_text_item_get_content(text1); REQUIRE(std::string(body1) == text_body2); + heif_string_release(body1); + char * elng1; + err = heif_item_get_property_extended_language(readbackCtx, id1, &elng1); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(strcmp(elng1, "en-UK") == 0); + heif_string_release(elng1); heif_text_item_release(text0); heif_text_item_release(text1);