Commit 2518e276 for libheif

commit 2518e2764174d42934f0606396fb866f94f10061
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Oct 5 01:38:59 2026 +0200

    Return an error for decoding and format queries on images of an encoding context

    The decoder object of an image item is created when the item is read from
    a file. An item that was added to the context by encoding has none, and
    get_decoder() returned that null pointer inside a successful result. The
    callers dereferenced it, so these calls on the handle returned by
    heif_context_encode_image() crashed for every codec:

    - heif_decode_image() and heif_image_handle_decode_image_tile()
      (except for the uncompressed codec, which has its own path),
    - heif_image_handle_get_luma_bits_per_pixel() and
      heif_image_handle_get_chroma_bits_per_pixel(),
    - heif_image_handle_get_preferred_decoding_colorspace(),
    - heif_image_handle_has_alpha_channel().

    Decoding an image from the context it was encoded into is not a supported
    use of the API and already crashed in v1.17.6. The queries returned values
    there; they go through the decoder object since v1.19.0.

    All item types now return their decoder through decoder_or_error(), which
    turns a missing decoder into a usage error, and the remaining direct uses
    of m_decoder are guarded. Decoding returns the error, the bit depth
    queries return -1, the colorspace query returns the error and the alpha
    query returns 0.

    Document in the public headers that a heif_context is used either for
    reading or for writing, and what the handle of an encoded image can be
    used for.

    The new test runs the queries and both decoding functions on an encoded
    handle for every available encoder.

diff --git a/libheif/api/libheif/heif_context.h b/libheif/api/libheif/heif_context.h
index 6a268c7f..36ae7e97 100644
--- a/libheif/api/libheif/heif_context.h
+++ b/libheif/api/libheif/heif_context.h
@@ -119,12 +119,15 @@ typedef enum heif_compression_format


 // ========================= heif_context =========================
-// A heif_context represents a HEIF file that has been read.
-// In the future, you will also be able to add pictures to a heif_context
-// and write it into a file again.
+// A heif_context represents a HEIF file. It is used either for reading a file
+// (heif_context_read_from_...() and decoding its images), or for writing a new file
+// (encoding images into the context and heif_context_write()), but not for both.
+// A context that has been read from a file cannot be written, and an image that has been
+// added to a context by encoding cannot be decoded from that context: write the file and
+// read it into a new context for that.


-// Allocate a new context for reading HEIF files.
+// Allocate a new context for reading or for writing a HEIF file.
 // Has to be freed again with heif_context_free().
 LIBHEIF_API
 heif_context* heif_context_alloc(void);
diff --git a/libheif/api/libheif/heif_encoding.h b/libheif/api/libheif/heif_encoding.h
index c87b849a..c771ff9f 100644
--- a/libheif/api/libheif/heif_encoding.h
+++ b/libheif/api/libheif/heif_encoding.h
@@ -346,6 +346,11 @@ void heif_encoding_options_free(heif_encoding_options*);
 // 'options' should be NULL for now.
 // The first image added to the context is also automatically set the primary image, but
 // you can change the primary image later with heif_context_set_primary_image().
+//
+// The returned handle refers to an image that is being written. Use it to attach thumbnails,
+// metadata or properties to the image, or to make it the primary image. It cannot be used
+// to decode the image, and the functions that report the coded format of an image (bit
+// depths, preferred colorspace, alpha channel) return an error or "unknown" for it.
 LIBHEIF_API
 heif_error heif_context_encode_image(heif_context*,
                                      const heif_image* image,
diff --git a/libheif/image-items/avc.cc b/libheif/image-items/avc.cc
index 88f15561..3b6b7abe 100644
--- a/libheif/image-items/avc.cc
+++ b/libheif/image-items/avc.cc
@@ -44,7 +44,7 @@ ImageItem_AVC::ImageItem_AVC(HeifContext* ctx)

 Result<std::shared_ptr<Decoder>> ImageItem_AVC::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/libheif/image-items/avif.cc b/libheif/image-items/avif.cc
index ea772931..584861c1 100644
--- a/libheif/image-items/avif.cc
+++ b/libheif/image-items/avif.cc
@@ -71,13 +71,18 @@ void ImageItem_AVIF::set_decoder_input_data()

 Result<std::vector<uint8_t>> ImageItem_AVIF::read_bitstream_configuration_data() const
 {
-  return m_decoder->read_bitstream_configuration_data();
+  auto decoderResult = get_decoder();
+  if (!decoderResult) {
+    return decoderResult.error();
+  }
+
+  return (*decoderResult)->read_bitstream_configuration_data();
 }


 Result<std::shared_ptr<Decoder>> ImageItem_AVIF::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/libheif/image-items/hevc.cc b/libheif/image-items/hevc.cc
index 7443ee36..2c32574c 100644
--- a/libheif/image-items/hevc.cc
+++ b/libheif/image-items/hevc.cc
@@ -116,13 +116,18 @@ bool ImageItem_HEVC::is_coded_in_miaf_profile() const

 Result<std::vector<uint8_t>> ImageItem_HEVC::read_bitstream_configuration_data() const
 {
-  return m_decoder->read_bitstream_configuration_data();
+  auto decoderResult = get_decoder();
+  if (!decoderResult) {
+    return decoderResult.error();
+  }
+
+  return (*decoderResult)->read_bitstream_configuration_data();
 }


 Result<std::shared_ptr<Decoder>> ImageItem_HEVC::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 3d544b1a..5d826516 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -510,6 +510,19 @@ Error ImageItem::postprocess_coded_image_colorspace(heif_colorspace* inout_color
 }


+Result<std::shared_ptr<Decoder>> ImageItem::decoder_or_error(std::shared_ptr<Decoder> decoder)
+{
+  if (!decoder) {
+    return Error{heif_error_Usage_error,
+                 heif_suberror_Unspecified,
+                 "The image was not read from a file. An image that was added to the context by "
+                 "encoding cannot be decoded and its coded format cannot be queried."};
+  }
+
+  return decoder;
+}
+
+
 Error ImageItem::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
 {
   auto decoderResult = get_decoder();
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index e7382497..100ede4c 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -474,6 +474,12 @@ public:

   Error transform_requested_tile_position_to_original_tile_position(uint32_t& tile_x, uint32_t& tile_y) const;

+  // The decoder of an image item is created when the item is read from a file
+  // (initialize_decoder()). An item that was added by encoding an image has none.
+  // This turns a missing decoder into an error, so that the functions that need the
+  // decoder fail instead of dereferencing a null pointer.
+  static Result<std::shared_ptr<class Decoder>> decoder_or_error(std::shared_ptr<class Decoder> decoder);
+
   virtual Result<std::shared_ptr<class Decoder>> get_decoder() const
   {
     return Error{
diff --git a/libheif/image-items/jpeg.cc b/libheif/image-items/jpeg.cc
index f8294d03..a47a9831 100644
--- a/libheif/image-items/jpeg.cc
+++ b/libheif/image-items/jpeg.cc
@@ -44,13 +44,18 @@ ImageItem_JPEG::ImageItem_JPEG(HeifContext* ctx)

 Result<std::vector<uint8_t>> ImageItem_JPEG::read_bitstream_configuration_data() const
 {
-  return m_decoder->read_bitstream_configuration_data();
+  auto decoderResult = get_decoder();
+  if (!decoderResult) {
+    return decoderResult.error();
+  }
+
+  return (*decoderResult)->read_bitstream_configuration_data();
 }


 Result<std::shared_ptr<Decoder>> ImageItem_JPEG::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/libheif/image-items/jpeg2000.cc b/libheif/image-items/jpeg2000.cc
index 8e2ef90a..6aa89892 100644
--- a/libheif/image-items/jpeg2000.cc
+++ b/libheif/image-items/jpeg2000.cc
@@ -63,7 +63,7 @@ Result<std::vector<uint8_t>> ImageItem_JPEG2000::read_bitstream_configuration_da

 Result<std::shared_ptr<Decoder>> ImageItem_JPEG2000::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
index 9310cd4c..5b3557f5 100644
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -479,7 +479,7 @@ heif_image_tiling ImageItem_uncompressed::get_heif_image_tiling() const

 Result<std::shared_ptr<Decoder>> ImageItem_uncompressed::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }

 std::shared_ptr<Encoder> ImageItem_uncompressed::get_encoder() const
@@ -536,6 +536,11 @@ void ImageItem_uncompressed::set_decoder_input_data()

 bool ImageItem_uncompressed::has_coded_alpha_channel() const
 {
+  // An item that was added by encoding has no decoder (see decoder_or_error()).
+  if (!m_decoder) {
+    return false;
+  }
+
   return m_decoder->has_alpha_component();
 }

diff --git a/libheif/image-items/vvc.cc b/libheif/image-items/vvc.cc
index ee2e66a5..fda4b134 100644
--- a/libheif/image-items/vvc.cc
+++ b/libheif/image-items/vvc.cc
@@ -65,7 +65,7 @@ Result<std::vector<uint8_t>> ImageItem_VVC::read_bitstream_configuration_data()

 Result<std::shared_ptr<Decoder>> ImageItem_VVC::get_decoder() const
 {
-  return {m_decoder};
+  return decoder_or_error(m_decoder);
 }


diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 62140dbf..1e9b994d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -112,6 +112,7 @@ add_libheif_test(error_item_decode)
 add_libheif_test(alpha_cycle_deadlock)
 add_libheif_test(alpha_composite_decode)
 add_libheif_test(encode_plane_layout)
+add_libheif_test(encode_handle_queries)
 add_libheif_test(write_to_file)
 add_libheif_test(parallel_grid_deadlock)
 # The deadlock regression tests decode on a worker thread guarded by a timeout.
diff --git a/tests/encode_handle_queries.cc b/tests/encode_handle_queries.cc
new file mode 100644
index 00000000..f3ccad09
--- /dev/null
+++ b/tests/encode_handle_queries.cc
@@ -0,0 +1,178 @@
+/*
+  libheif unit tests
+
+  MIT License
+
+  Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+// A heif_context is used either for reading or for writing. The decoder object of an image
+// item is only created when the item is read from a file, so an image that was added to the
+// context by encoding has none. Decoding such an image, and also the plain queries for its
+// bit depth, colorspace and alpha channel on the handle that heif_context_encode_image()
+// returns, dereferenced the missing decoder and crashed. This is not a supported use of the
+// API, but it has to fail with an error.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
+#include <string>
+
+namespace {
+
+constexpr int W = 64;
+constexpr int H = 64;
+
+heif_image* create_rgb_image()
+{
+  heif_image* img = nullptr;
+  heif_error err = heif_image_create(W, H, heif_colorspace_RGB, heif_chroma_444, &img);
+  REQUIRE(err.code == heif_error_Ok);
+
+  for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    err = heif_image_add_plane(img, channel, W, H, 8);
+    REQUIRE(err.code == heif_error_Ok);
+
+    size_t stride = 0;
+    uint8_t* p = heif_image_get_plane2(img, channel, &stride);
+    REQUIRE(p != nullptr);
+    for (int y = 0; y < H; y++) {
+      memset(p + y * stride, 0x40, W);
+    }
+  }
+
+  return img;
+}
+
+const char* format_name(heif_compression_format format)
+{
+  switch (format) {
+    case heif_compression_HEVC: return "HEVC";
+    case heif_compression_AVC: return "AVC";
+    case heif_compression_JPEG: return "JPEG";
+    case heif_compression_AV1: return "AV1";
+    case heif_compression_VVC: return "VVC";
+    case heif_compression_JPEG2000: return "JPEG 2000";
+    case heif_compression_uncompressed: return "uncompressed";
+    case heif_compression_HTJ2K: return "HT-J2K";
+    default: return "?";
+  }
+}
+
+} // namespace
+
+
+TEST_CASE("queries and decoding on the handle of an encoded image fail without crashing")
+{
+  int num_formats_tested = 0;
+
+  for (heif_compression_format format : {heif_compression_HEVC, heif_compression_AVC, heif_compression_JPEG,
+                                         heif_compression_AV1, heif_compression_VVC, heif_compression_JPEG2000,
+                                         heif_compression_uncompressed, heif_compression_HTJ2K}) {
+    if (!heif_have_encoder_for_format(format)) {
+      continue;
+    }
+
+    INFO("format: " << format_name(format));
+
+    heif_image* img = create_rgb_image();
+    heif_context* ctx = heif_context_alloc();
+
+    heif_encoder* encoder = nullptr;
+    heif_error err = heif_context_get_encoder_for_format(ctx, format, &encoder);
+    REQUIRE(err.code == heif_error_Ok);
+
+    heif_image_handle* handle = nullptr;
+    err = heif_context_encode_image(ctx, img, encoder, nullptr, &handle);
+    if (err.code != heif_error_Ok) {
+      // an encoder that does not take this kind of image is not what is tested here
+      heif_encoder_release(encoder);
+      heif_context_free(ctx);
+      heif_image_release(img);
+      continue;
+    }
+    REQUIRE(handle != nullptr);
+    num_formats_tested++;
+
+    // --- these work on a handle of an image that is being written
+
+    CHECK(heif_image_handle_get_width(handle) == W);
+    CHECK(heif_image_handle_get_height(handle) == H);
+
+    // --- these need the decoder of the image item, which an encoded image does not have
+
+    // The bit depth is unknown (-1).
+    CHECK(heif_image_handle_get_luma_bits_per_pixel(handle) == -1);
+    CHECK(heif_image_handle_get_chroma_bits_per_pixel(handle) == -1);
+
+    heif_colorspace colorspace;
+    heif_chroma chroma;
+    err = heif_image_handle_get_preferred_decoding_colorspace(handle, &colorspace, &chroma);
+    CHECK(err.code != heif_error_Ok);
+
+    CHECK(heif_image_handle_has_alpha_channel(handle) == 0);
+
+    // The uncompressed codec happens to decode from an encoding context. That is not
+    // required. What is required is that no format crashes and that a failure is
+    // reported together with a null image.
+    heif_image* decoded = nullptr;
+    err = heif_decode_image(handle, &decoded, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+    if (format != heif_compression_uncompressed) {
+      CHECK(err.code != heif_error_Ok);
+    }
+    CHECK((err.code == heif_error_Ok) == (decoded != nullptr));
+    if (decoded) {
+      heif_image_release(decoded);
+    }
+
+    decoded = nullptr;
+    err = heif_image_handle_decode_image_tile(handle, &decoded, heif_colorspace_undefined, heif_chroma_undefined,
+                                              nullptr, 0, 0);
+    if (format != heif_compression_uncompressed) {
+      CHECK(err.code != heif_error_Ok);
+    }
+    CHECK((err.code == heif_error_Ok) == (decoded != nullptr));
+    if (decoded) {
+      heif_image_release(decoded);
+    }
+
+    // --- the same through the primary image handle of the context
+
+    heif_image_handle* primary = nullptr;
+    err = heif_context_get_primary_image_handle(ctx, &primary);
+    REQUIRE(err.code == heif_error_Ok);
+    CHECK(heif_image_handle_get_luma_bits_per_pixel(primary) == -1);
+    CHECK(heif_image_handle_has_alpha_channel(primary) == 0);
+    heif_image_handle_release(primary);
+
+    heif_image_handle_release(handle);
+    heif_encoder_release(encoder);
+    heif_context_free(ctx);
+    heif_image_release(img);
+  }
+
+  if (num_formats_tested == 0) {
+    SKIP("no encoder available");
+  }
+}