Commit edfd19dd for libheif
commit edfd19ddec734974bb22e71158141c15f8192289
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Aug 27 02:53:12 2026 +0200
extend the item writing tests
- Pin the allocated IDs to 1..4 and explain why the seed item exists: for
heif_context_add_precompressed_mime_item the old buggy value (1) was itself a
'mime' item, so the type checks could not tell the fix from the bug.
- Use heif_item_type_mime / heif_item_type_uri instead of hand-spelled fourccs,
and "" instead of "identity" as content_encoding.
- New cases: argument validation (NULL strings, negative size, NULL data), reading
item data back before writing, fresh IDs when adding to a loaded file, a full
round trip through an encoded file including an item with an undecodable
content_encoding, and the refusal to write a context without images.
- Keep the test list in tests/CMakeLists.txt alphabetical.
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index acfe1873..dbf341e59 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -75,9 +75,9 @@ add_libheif_test(encode)
add_libheif_test(encode_grid)
add_libheif_test(entity_groups)
add_libheif_test(extended_type)
-add_libheif_test(item_writing)
add_libheif_test(grid_tile_missing)
add_libheif_test(iden_declared_size)
+add_libheif_test(item_writing)
add_libheif_test(overlay_amplification)
add_libheif_test(region)
add_libheif_test(sequence_no_track)
diff --git a/tests/item_writing.cc b/tests/item_writing.cc
index 25a0f4f9..0df14c77 100644
--- a/tests/item_writing.cc
+++ b/tests/item_writing.cc
@@ -27,8 +27,35 @@
#include "catch_amalgamated.hpp"
#include "libheif/heif.h"
#include "libheif/heif_items.h"
+#include "test_utils.h"
+#include <algorithm>
#include <array>
+#include <string>
+#include <vector>
+
+
+static std::vector<heif_item_id> get_item_ids(const heif_context* ctx)
+{
+ int n = heif_context_get_number_of_items(ctx);
+ std::vector<heif_item_id> ids(n);
+ REQUIRE(heif_context_get_list_of_item_IDs(ctx, ids.data(), n) == n);
+ return ids;
+}
+
+
+static std::vector<uint8_t> read_item_data(const heif_context* ctx, heif_item_id id,
+ heif_metadata_compression* out_compression = nullptr)
+{
+ uint8_t* data = nullptr;
+ size_t size = 0;
+ heif_error err = heif_item_get_item_data(ctx, id, out_compression, &data, &size);
+ REQUIRE(err.code == heif_error_Ok);
+
+ std::vector<uint8_t> result(data, data + size);
+ heif_release_item_data(ctx, &data);
+ return result;
+}
TEST_CASE("generic item writers return the allocated item IDs")
@@ -42,6 +69,9 @@ TEST_CASE("generic item writers return the allocated item IDs")
heif_item_id compressed_mime_id = 0;
heif_item_id uri_id = 0;
+ // The bug this guards against (PR #1885) made the writers store the boolean "success"
+ // value 1 instead of the item ID. The seed item takes ID 1 first, so that a regression
+ // to the old behaviour cannot be mistaken for a correct ID below.
heif_error err = heif_context_add_mime_item(ctx, "application/octet-stream",
heif_metadata_compression_off,
data.data(), data.size(), &seed_id);
@@ -50,24 +80,318 @@ TEST_CASE("generic item writers return the allocated item IDs")
err = heif_context_add_item(ctx, "test", data.data(), data.size(), &item_id);
REQUIRE(err.code == heif_error_Ok);
- err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", "identity",
+ err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", "",
data.data(), data.size(), &compressed_mime_id);
REQUIRE(err.code == heif_error_Ok);
err = heif_context_add_uri_item(ctx, "urn:example:test", data.data(), data.size(), &uri_id);
REQUIRE(err.code == heif_error_Ok);
- REQUIRE(seed_id != 0);
- REQUIRE(item_id != seed_id);
- REQUIRE(compressed_mime_id != seed_id);
- REQUIRE(compressed_mime_id != item_id);
- REQUIRE(uri_id != seed_id);
- REQUIRE(uri_id != item_id);
- REQUIRE(uri_id != compressed_mime_id);
+ // IDs are handed out sequentially, starting at 1 (tests/region.cc relies on this as well).
+ REQUIRE(seed_id == 1);
+ REQUIRE(item_id == 2);
+ REQUIRE(compressed_mime_id == 3);
+ REQUIRE(uri_id == 4);
+ REQUIRE(heif_item_get_item_type(ctx, seed_id) == heif_item_type_mime);
REQUIRE(heif_item_get_item_type(ctx, item_id) == heif_fourcc('t', 'e', 's', 't'));
- REQUIRE(heif_item_get_item_type(ctx, compressed_mime_id) == heif_fourcc('m', 'i', 'm', 'e'));
- REQUIRE(heif_item_get_item_type(ctx, uri_id) == heif_fourcc('u', 'r', 'i', ' '));
+ REQUIRE(heif_item_get_item_type(ctx, compressed_mime_id) == heif_item_type_mime);
+ REQUIRE(heif_item_get_item_type(ctx, uri_id) == heif_item_type_uri);
+
+ REQUIRE(std::string(heif_item_get_uri_item_uri_type(ctx, uri_id)) == "urn:example:test");
+
+ std::vector<heif_item_id> ids = get_item_ids(ctx);
+ std::sort(ids.begin(), ids.end());
+ REQUIRE(ids == std::vector<heif_item_id>{1, 2, 3, 4});
+
+ // out_item_id may be NULL: the item is still added
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off,
+ data.data(), data.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(heif_context_get_number_of_items(ctx) == 5);
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("item writers reject invalid arguments without adding an item")
+{
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ const std::array<uint8_t, 4> data = {1, 2, 3, 4};
+ heif_item_id id = 0;
+ heif_error err;
+
+ // NULL strings
+ err = heif_context_add_mime_item(ctx, nullptr, heif_metadata_compression_off, data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_precompressed_mime_item(ctx, nullptr, "", data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", nullptr, data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_uri_item(ctx, nullptr, data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_item(ctx, nullptr, data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_item(ctx, "toolong", data.data(), 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+
+ // negative size
+ err = heif_context_add_item(ctx, "test", data.data(), -1, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, data.data(), -1, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "", data.data(), -1, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_uri_item(ctx, "urn:example:test", data.data(), -1, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+
+ // NULL data with non-zero size
+ err = heif_context_add_item(ctx, "test", nullptr, 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, nullptr, 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "", nullptr, 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+ err = heif_context_add_uri_item(ctx, "urn:example:test", nullptr, 4, &id);
+ REQUIRE(err.code == heif_error_Usage_error);
+
+ REQUIRE(heif_context_get_number_of_items(ctx) == 0);
+
+ // NULL data with size 0 is an empty item
+ err = heif_context_add_item(ctx, "test", nullptr, 0, &id);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(heif_context_get_number_of_items(ctx) == 1);
+ REQUIRE(read_item_data(ctx, id).empty());
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("item data can be read back from a context that is being written")
+{
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ const std::vector<uint8_t> payload = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
+ heif_item_id id = 0;
+ heif_error err;
+ heif_metadata_compression compression;
+
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off,
+ payload.data(), (int) payload.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(read_item_data(ctx, id, &compression) == payload);
+ REQUIRE(compression == heif_metadata_compression_off);
+ REQUIRE(std::string(heif_item_get_mime_item_content_type(ctx, id)) == "text/plain");
+
+ err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(read_item_data(ctx, id) == payload);
+
+ err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(read_item_data(ctx, id) == payload);
+
+ // "identity" is the RFC 2616 no-op content coding
+ err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "identity",
+ payload.data(), (int) payload.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(read_item_data(ctx, id) == payload);
+
+ // compressed mime item: either it round-trips, or (without the compressor) it is rejected cleanly
+ int num_items = heif_context_get_number_of_items(ctx);
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_deflate,
+ payload.data(), (int) payload.size(), &id);
+ if (heif_metadata_compression_method_supported(heif_metadata_compression_deflate)) {
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, id)) == "deflate");
+ REQUIRE(read_item_data(ctx, id) == payload);
+
+ std::vector<uint8_t> compressed = read_item_data(ctx, id, &compression);
+ REQUIRE(compression == heif_metadata_compression_deflate);
+ REQUIRE(compressed != payload);
+ }
+ else {
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(heif_context_get_number_of_items(ctx) == num_items);
+ }
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("items added to a loaded file get fresh IDs")
+{
+ heif_context* ctx = get_context_for_test_file("rainbow-451x461.heic");
+
+ std::vector<heif_item_id> existing_ids = get_item_ids(ctx);
+ REQUIRE(!existing_ids.empty());
+ std::vector<uint32_t> existing_types;
+ for (heif_item_id id : existing_ids) {
+ existing_types.push_back(heif_item_get_item_type(ctx, id));
+ }
+
+ const std::vector<uint8_t> payload = {'n', 'e', 'w'};
+ heif_item_id mime_id = 0, uri_id = 0, generic_id = 0;
+ heif_error err;
+
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off,
+ payload.data(), (int) payload.size(), &mime_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &uri_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &generic_id);
+ REQUIRE(err.code == heif_error_Ok);
+
+ // The new IDs must not collide with the items read from the file (the ID allocator used to
+ // restart at 1 and silently replaced the primary image) ...
+ for (heif_item_id new_id : {mime_id, uri_id, generic_id}) {
+ REQUIRE(std::find(existing_ids.begin(), existing_ids.end(), new_id) == existing_ids.end());
+ }
+ REQUIRE(mime_id != uri_id);
+ REQUIRE(uri_id != generic_id);
+ REQUIRE(heif_context_get_number_of_items(ctx) == (int) existing_ids.size() + 3);
+
+ // ... and the existing items must be untouched
+ for (size_t i = 0; i < existing_ids.size(); i++) {
+ REQUIRE(heif_item_get_item_type(ctx, existing_ids[i]) == existing_types[i]);
+ }
+
+ heif_image_handle* handle = get_primary_image_handle(ctx);
+ REQUIRE(heif_image_handle_get_width(handle) == 451);
+ heif_image_handle_release(handle);
+
+ // the new data is readable even though the context has an input file that does not contain it
+ REQUIRE(read_item_data(ctx, mime_id) == payload);
+ REQUIRE(read_item_data(ctx, uri_id) == payload);
+ REQUIRE(read_item_data(ctx, generic_id) == payload);
+
+ // Writing back a context that was read from a file is not implemented (the image data
+ // would not be copied). This must be reported as an error, not crash or write a
+ // truncated file.
+ std::string path = get_tests_output_file_path("item_writing_loaded_context.heic");
+ err = heif_context_write_to_file(ctx, path.c_str());
+ REQUIRE(err.code != heif_error_Ok);
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("items survive a round trip through a written file")
+{
+ heif_context* ctx = heif_context_alloc();
+ heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC);
+
+ heif_image* img = createImage_RGB_planar();
+ const int image_width = heif_image_get_width(img, heif_channel_R);
+
+ heif_image_handle* handle = nullptr;
+ heif_error err = heif_context_encode_image(ctx, img, enc, nullptr, &handle);
+ REQUIRE(err.code == heif_error_Ok);
+ heif_item_id image_id = heif_image_handle_get_item_id(handle);
+ heif_image_handle_release(handle);
+ heif_image_release(img);
+ heif_encoder_release(enc);
+
+ const std::vector<uint8_t> payload = {'n', 'e', 'w'};
+ const std::vector<uint8_t> opaque = {0xde, 0xad, 0xbe, 0xef};
+ heif_item_id mime_id = 0, uri_id = 0, generic_id = 0, identity_id = 0, unknown_id = 0;
+
+ err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off,
+ payload.data(), (int) payload.size(), &mime_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &uri_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &generic_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "identity",
+ payload.data(), (int) payload.size(), &identity_id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", "x-unknown-coding",
+ opaque.data(), (int) opaque.size(), &unknown_id);
+ REQUIRE(err.code == heif_error_Ok);
+
+ std::vector<heif_item_id> ids = {image_id, mime_id, uri_id, generic_id, identity_id, unknown_id};
+ std::sort(ids.begin(), ids.end());
+ REQUIRE(std::unique(ids.begin(), ids.end()) == ids.end());
+
+ std::string path = get_tests_output_file_path("item_writing_roundtrip.heic");
+ err = heif_context_write_to_file(ctx, path.c_str());
+ REQUIRE(err.code == heif_error_Ok);
+ heif_context_free(ctx);
+
+ // --- read back (must succeed although one item has an undecodable content_encoding)
+
+ ctx = get_context_for_local_file(path);
+ REQUIRE(heif_context_get_number_of_items(ctx) == 6);
+
+ // the item IDs are preserved
+ REQUIRE(heif_item_get_item_type(ctx, mime_id) == heif_item_type_mime);
+ REQUIRE(std::string(heif_item_get_mime_item_content_type(ctx, mime_id)) == "text/plain");
+ REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, mime_id)) == "");
+ REQUIRE(read_item_data(ctx, mime_id) == payload);
+
+ REQUIRE(heif_item_get_item_type(ctx, uri_id) == heif_item_type_uri);
+ REQUIRE(std::string(heif_item_get_uri_item_uri_type(ctx, uri_id)) == "urn:example:test");
+ REQUIRE(read_item_data(ctx, uri_id) == payload);
+
+ REQUIRE(heif_item_get_item_type(ctx, generic_id) == heif_fourcc('t', 'e', 's', 't'));
+ REQUIRE(read_item_data(ctx, generic_id) == payload);
+
+ REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, identity_id)) == "identity");
+ REQUIRE(read_item_data(ctx, identity_id) == payload);
+
+ // the undecodable item: raw data is available, decoding is refused
+ REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, unknown_id)) == "x-unknown-coding");
+ heif_metadata_compression compression;
+ REQUIRE(read_item_data(ctx, unknown_id, &compression) == opaque);
+ REQUIRE(compression == heif_metadata_compression_unknown);
+
+ uint8_t* data = nullptr;
+ size_t size = 0;
+ err = heif_item_get_item_data(ctx, unknown_id, nullptr, &data, &size);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(data == nullptr);
+
+ // the image is intact
+ heif_item_id primary_id = 0;
+ REQUIRE(heif_context_get_primary_image_ID(ctx, &primary_id).code == heif_error_Ok);
+ REQUIRE(primary_id == image_id);
+
+ handle = get_primary_image_handle(ctx);
+ REQUIRE(heif_image_handle_get_width(handle) == image_width);
+ if (heif_have_decoder_for_format(heif_compression_HEVC)) {
+ img = get_primary_image(handle);
+ REQUIRE(heif_image_get_primary_width(img) == image_width);
+ heif_image_release(img);
+ }
+ heif_image_handle_release(handle);
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("a context without images or sequences cannot be written")
+{
+ const std::array<uint8_t, 2> data = {0x12, 0x34};
+ std::string path = get_tests_output_file_path("item_writing_no_images.heic");
+
+ heif_context* ctx = heif_context_alloc();
+ heif_item_id id = 0;
+ heif_error err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off,
+ data.data(), data.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_write_to_file(ctx, path.c_str());
+ REQUIRE(err.code == heif_error_Usage_error);
+ heif_context_free(ctx);
+
+ ctx = heif_context_alloc();
+ err = heif_context_add_item(ctx, "test", data.data(), data.size(), &id);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_context_write_to_file(ctx, path.c_str());
+ REQUIRE(err.code == heif_error_Usage_error);
heif_context_free(ctx);
}