Commit 9ec655f7 for libheif

commit 9ec655f76b6fe298092aaeb063f821f53b8eb901
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sun Oct 25 00:27:32 2026 +0200

    Reject shrinking padding and mixed-size sequence frames [GHSA-hpg4-g2gc-pq42]

diff --git a/libheif/api/libheif/heif_image.cc b/libheif/api/libheif/heif_image.cc
index 144b07c9..d61b7904 100644
--- a/libheif/api/libheif/heif_image.cc
+++ b/libheif/api/libheif/heif_image.cc
@@ -400,6 +400,12 @@ int heif_image_is_premultiplied_alpha(heif_image* image)

 heif_error heif_image_extend_padding_to_size(heif_image* image, int min_physical_width, int min_physical_height)
 {
+  if (min_physical_width < 0 || min_physical_height < 0) {
+    return Error{heif_error_Usage_error,
+                 heif_suberror_Invalid_parameter_value,
+                 "Cannot extend padding to a negative size."}.error_struct(image->image.get());
+  }
+
   Error err = image->image->extend_padding_to_size(min_physical_width, min_physical_height, false, nullptr);
   if (err) {
     return err.error_struct(image->image.get());
diff --git a/libheif/api/libheif/heif_image.h b/libheif/api/libheif/heif_image.h
index 30d6822b..ce2393f7 100644
--- a/libheif/api/libheif/heif_image.h
+++ b/libheif/api/libheif/heif_image.h
@@ -426,8 +426,9 @@ int heif_image_is_premultiplied_alpha(heif_image* image);
 // This function extends the padding of the image so that it has at least the given physical size.
 // The padding border is filled with the pixels along the right/bottom border.
 // This function may be useful if you want to process the image, but have some external padding requirements.
-// The image size will not be modified if it is already larger/equal than the given physical size.
-// I.e. you cannot assume that after calling this function, the stride will be equal to min_physical_width.
+// Each requested dimension must be at least as large as the corresponding image dimension;
+// otherwise an error is returned. The logical image size is not modified.
+// Existing padding may be larger, so the stride need not equal min_physical_width.
 LIBHEIF_API
 heif_error heif_image_extend_padding_to_size(heif_image* image,
                                              int min_physical_width, int min_physical_height);
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index 7c57a1c3..0ed8e6f2 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -608,7 +608,15 @@ Error HeifPixelImage::ComponentStorage::alloc(uint32_t width, uint32_t height, h
 Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bool adjust_size,
                                              const heif_security_limits* limits)
 {
-  for (auto& component : m_storage) {
+  if (width < m_width || height < m_height) {
+    return Error{heif_error_Usage_error,
+                 heif_suberror_Invalid_parameter_value,
+                 "Cannot extend padding to a size smaller than the image."};
+  }
+
+  // Check every plane before changing any of them. In particular, the new
+  // allocation must fit the old plane in both directions before it is copied.
+  for (const auto& component : m_storage) {
     // get_subsampled_size() only adjusts the size for Cb/Cr; for every other
     // channel it assumes the component has the full logical image size. We
     // cannot compute a correct padded size for a non-Cb/Cr component that does
@@ -622,6 +630,19 @@ Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bo
                    "Cannot extend padding for an image with non-uniform component sizes."};
     }

+    uint32_t subsampled_width, subsampled_height;
+    get_subsampled_size(width, height, component.m_channel, m_chroma,
+                        &subsampled_width, &subsampled_height);
+
+    if (subsampled_width < component.m_width ||
+        subsampled_height < component.m_height) {
+      return Error{heif_error_Usage_error,
+                   heif_suberror_Invalid_parameter_value,
+                   "Cannot extend padding to a size smaller than an existing plane."};
+    }
+  }
+
+  for (auto& component : m_storage) {
     uint32_t subsampled_width, subsampled_height;
     get_subsampled_size(width, height, component.m_channel, m_chroma,
                         &subsampled_width, &subsampled_height);
@@ -663,6 +684,10 @@ Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bo
       std::free(component.allocated_mem);

       component = newPlane;
+      // alloc() set the logical plane size to the allocation size. Growing
+      // padding alone must not change the plane's visible dimensions.
+      component.m_width = old_width;
+      component.m_height = old_height;
     }

     // extend plane size
diff --git a/libheif/plugins/encoder_aom.cc b/libheif/plugins/encoder_aom.cc
index d891a353..5a5eae3a 100644
--- a/libheif/plugins/encoder_aom.cc
+++ b/libheif/plugins/encoder_aom.cc
@@ -100,6 +100,8 @@ struct encoder_struct_aom

   // bit depth the codec was initialized with, to check the later frames of a sequence against
   int bit_depth = 8;
+  int input_width = 0;
+  int input_height = 0;

   // --- input

@@ -936,6 +938,8 @@ static heif_error aom_start_sequence_encoding_intern(void* encoder_raw, const he

   const int source_width = heif_image_get_width(image, heif_channel_Y);
   const int source_height = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = source_width;
+  encoder->input_height = source_height;

   const heif_chroma chroma = heif_image_get_chroma_format(image);

@@ -1285,6 +1289,13 @@ static heif_error aom_encode_sequence_frame(void* encoder_raw, const heif_image*
     return input_error;
   }

+  // libaom can change resolution after reconfiguring the encoder. This plugin
+  // does not do that yet; lift this check when we support mixed-size sequences.
+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   heif_error err;

   const int source_width = heif_image_get_width(image, heif_channel_Y);
diff --git a/libheif/plugins/encoder_input_check.h b/libheif/plugins/encoder_input_check.h
index 127fd762..71278f07 100644
--- a/libheif/plugins/encoder_input_check.h
+++ b/libheif/plugins/encoder_input_check.h
@@ -160,4 +160,21 @@ static inline heif_error check_sequence_frame_bit_depth(const heif_image* image,
   return heif_error_ok;
 }

+
+// These plugins open one encoder instance with dimensions taken from the first
+// frame. Reject later frames of a different size before handing their planes
+// to an encoder configured for the old size.
+static inline heif_error check_sequence_frame_size(const heif_image* image,
+                                                   int configured_width, int configured_height)
+{
+  if (heif_image_get_width(image, heif_channel_Y) != configured_width ||
+      heif_image_get_height(image, heif_channel_Y) != configured_height) {
+    return heif_error{heif_error_Invalid_input,
+                      heif_suberror_Unspecified,
+                      "All frames of a sequence must have the size of the first frame"};
+  }
+
+  return heif_error_ok;
+}
+
 #endif // LIBHEIF_ENCODER_INPUT_CHECK_H
diff --git a/libheif/plugins/encoder_kvazaar.cc b/libheif/plugins/encoder_kvazaar.cc
index dcad1eb8..b62e7bd3 100644
--- a/libheif/plugins/encoder_kvazaar.cc
+++ b/libheif/plugins/encoder_kvazaar.cc
@@ -59,6 +59,8 @@ struct encoder_struct_kvazaar
   const kvz_api* api = nullptr;
   kvz_config* config = nullptr;
   kvz_encoder* kvzencoder = nullptr;
+  int input_width = 0;
+  int input_height = 0;

   // --- output

@@ -518,6 +520,8 @@ static heif_error kvazaar_start_sequence_encoding_intern(void* encoder_raw, cons

   int input_width = heif_image_get_width(image, heif_channel_Y);
   int input_height = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = input_width;
+  encoder->input_height = input_height;

   uint32_t encoded_width, encoded_height;
   kvazaar_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height);
@@ -684,6 +688,11 @@ static heif_error kvazaar_encode_sequence_frame(void* encoder_raw, const heif_im

   encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw;

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   // Note: it is ok to cast away the const, as the image content is not changed.
   // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function.
   /*
diff --git a/libheif/plugins/encoder_rav1e.cc b/libheif/plugins/encoder_rav1e.cc
index f834e525..106362d3 100644
--- a/libheif/plugins/encoder_rav1e.cc
+++ b/libheif/plugins/encoder_rav1e.cc
@@ -63,6 +63,8 @@ struct encoder_struct_rav1e

   // bit depth the context was created with, to check the later frames of a sequence against
   int bit_depth = 8;
+  int input_width = 0;
+  int input_height = 0;

   // --- output

@@ -609,6 +611,8 @@ heif_error rav1e_start_sequence_encoding_intern(void* encoder_raw, const heif_im
   if (rav1e_config_parse_int(rav1eConfig.get(), "height", heif_image_get_height(image, heif_channel_Y)) == -1) {
     return heif_error_codec_library_error;
   }
+  encoder->input_width = heif_image_get_width(image, heif_channel_Y);
+  encoder->input_height = heif_image_get_height(image, heif_channel_Y);
   if (rav1e_config_parse_int(rav1eConfig.get(), "threads", encoder->threads) == -1) {
     return heif_error_codec_library_error;
   }
@@ -699,6 +703,11 @@ heif_error rav1e_encode_sequence_frame(void* encoder_raw, const heif_image* imag
     return input_error;
   }

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   int bitDepth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y);

   int yShift = encoder->yShift;
diff --git a/libheif/plugins/encoder_svt.cc b/libheif/plugins/encoder_svt.cc
index 4ce65970..bcec5684 100644
--- a/libheif/plugins/encoder_svt.cc
+++ b/libheif/plugins/encoder_svt.cc
@@ -90,6 +90,8 @@ struct encoder_struct_svt

   EbComponentType* svt_encoder = nullptr;
   EbSvtAv1EncConfiguration svt_config{}; // value-initialized: svt_encode_sequence_frame() reads encoder_bit_depth from it
+  int input_width = 0;
+  int input_height = 0;
   EbBufferHeaderType input_buffer;

   bool still_image_mode = false;
@@ -747,6 +749,8 @@ static heif_error svt_start_sequence_encoding_intern(void* encoder_raw, const he

   int w = heif_image_get_width(image, heif_channel_Y);
   int h = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = w;
+  encoder->input_height = h;

   uint32_t encoded_width, encoded_height;
   svt_query_encoded_size(encoder_raw, w, h, &encoded_width, &encoded_height);
@@ -1042,6 +1046,11 @@ static heif_error svt_encode_sequence_frame(void* encoder_raw, const heif_image*
     return input_error;
   }

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   int w = heif_image_get_width(image, heif_channel_Y);
   int h = heif_image_get_height(image, heif_channel_Y);
   const heif_chroma chroma = heif_image_get_chroma_format(image);
diff --git a/libheif/plugins/encoder_uvg266.cc b/libheif/plugins/encoder_uvg266.cc
index f6559ff0..5809328b 100644
--- a/libheif/plugins/encoder_uvg266.cc
+++ b/libheif/plugins/encoder_uvg266.cc
@@ -62,6 +62,8 @@ struct encoder_struct_uvg266
   int chroma_height_shift = 0;
   int input_chroma_width = 0;
   int input_chroma_height = 0;
+  int input_width = 0;
+  int input_height = 0;

   // --- output data

@@ -522,6 +524,8 @@ static heif_error uvg266_start_sequence_encoding_intern(void* encoder_raw, const

   int input_width = heif_image_get_width(image, heif_channel_Y);
   int input_height = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = input_width;
+  encoder->input_height = input_height;

   if (isGreyscale) {
     config->input_format = UVG_FORMAT_P400;
@@ -672,6 +676,11 @@ static heif_error uvg266_encode_sequence_frame(void* encoder_raw, const heif_ima

   encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw;

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome);

   int input_width = heif_image_get_width(image, heif_channel_Y);
diff --git a/libheif/plugins/encoder_vvenc.cc b/libheif/plugins/encoder_vvenc.cc
index 6e1f8305..d80584e5 100644
--- a/libheif/plugins/encoder_vvenc.cc
+++ b/libheif/plugins/encoder_vvenc.cc
@@ -54,6 +54,8 @@ struct encoder_struct_vvenc
   vvencEncoder* vvencoder = nullptr;
   vvencChromaFormat vvencChroma;
   uint32_t encoded_width=0, encoded_height=0;
+  int input_width = 0;
+  int input_height = 0;

   // --- output

@@ -455,6 +457,8 @@ static heif_error vvenc_start_sequence_encoding_intern(void* encoder_raw, const

   int input_width = heif_image_get_width(image, heif_channel_Y);
   int input_height = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = input_width;
+  encoder->input_height = input_height;

   vvenc_query_encoded_size(encoder_raw, input_width, input_height,
     &encoder->encoded_width, &encoder->encoded_height);
@@ -566,6 +570,14 @@ static heif_error vvenc_encode_sequence_frame(void* encoder_raw, const heif_imag
   }

   encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw;
+
+  // VVenC supports resolution changes with suitable maximum picture-size
+  // configuration. This plugin does not set that up yet; lift this check when
+  // we support mixed-size sequences.
+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
   vvencEncoder* vvencoder = encoder->vvencoder;

   bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome);
diff --git a/libheif/plugins/encoder_x264.cc b/libheif/plugins/encoder_x264.cc
index fea7696b..5ce024b6 100644
--- a/libheif/plugins/encoder_x264.cc
+++ b/libheif/plugins/encoder_x264.cc
@@ -96,6 +96,8 @@ struct encoder_struct_x264

   uintptr_t out_frameNr = 0;
   int bit_depth = 0;
+  int input_width = 0;
+  int input_height = 0;

   heif_chroma chroma;

@@ -927,6 +929,8 @@ static heif_error x264_start_sequence_encoding_intern(void* encoder_raw, const h

   param.i_width = heif_image_get_width(image, heif_channel_Y);
   param.i_height = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = param.i_width;
+  encoder->input_height = param.i_height;
   //param->internalBitDepth = bit_depth;

   param.i_width = rounded_size(param.i_width);
@@ -1015,6 +1019,11 @@ static heif_error x264_encode_sequence_frame(void* encoder_raw, const heif_image
     return input_error;
   }

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   heif_error err;

   // Note: it is ok to cast away the const, as the image content is not changed.
diff --git a/libheif/plugins/encoder_x265.cc b/libheif/plugins/encoder_x265.cc
index 87e2a03a..35aa120b 100644
--- a/libheif/plugins/encoder_x265.cc
+++ b/libheif/plugins/encoder_x265.cc
@@ -64,6 +64,8 @@ struct encoder_struct_x265
   x265_param* param = nullptr;

   int bit_depth = 0;
+  int input_width = 0;
+  int input_height = 0;

   heif_chroma chroma;

@@ -1038,6 +1040,8 @@ static heif_error x265_start_sequence_encoding_intern(void* encoder_raw, const h

   param->sourceWidth = heif_image_get_width(image, heif_channel_Y);
   param->sourceHeight = heif_image_get_height(image, heif_channel_Y);
+  encoder->input_width = param->sourceWidth;
+  encoder->input_height = param->sourceHeight;
   param->internalBitDepth = bit_depth;

   param->sourceWidth = rounded_size(param->sourceWidth);
@@ -1130,6 +1134,11 @@ static heif_error x265_encode_sequence_frame(void* encoder_raw, const heif_image
     return input_error;
   }

+  input_error = check_sequence_frame_size(image, encoder->input_width, encoder->input_height);
+  if (input_error.code != heif_error_Ok) {
+    return input_error;
+  }
+
   const x265_api* api = encoder->api;

   heif_error err;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 80275385..8ece6275 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -108,6 +108,7 @@ add_libheif_test(clap_decode)
 add_libheif_test(encode)
 add_libheif_test(encode_grid)
 add_libheif_test(encode_hevc_bitdepth)
+add_libheif_test(sequence_mixed_frame_size)
 add_libheif_test(entity_groups)
 add_libheif_test(extended_type)
 add_libheif_test(zero_length_memcpy)
diff --git a/tests/extend_to_size_checks.cc b/tests/extend_to_size_checks.cc
index 44090dcc..92298232 100644
--- a/tests/extend_to_size_checks.cc
+++ b/tests/extend_to_size_checks.cc
@@ -26,6 +26,9 @@

 #include "image/pixelimage.h"
 #include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+
+#include <algorithm>

 // Regression tests for HeifPixelImage::extend_to_size_with_zero().
 //
@@ -113,3 +116,82 @@ TEST_CASE("extend_to_size_with_zero accepts an unchanged size") {
   REQUIRE(image->get_width() == 8);
   REQUIRE(image->get_height() == 8);
 }
+
+
+static void check_smaller_padding_request(uint32_t width, uint32_t height,
+                                          uint32_t requested_width, uint32_t requested_height)
+{
+  auto* limits = heif_get_global_security_limits();
+  auto image = std::make_shared<HeifPixelImage>();
+  image->create(width, height, heif_colorspace_monochrome, heif_chroma_monochrome);
+  REQUIRE(image->add_channel(heif_channel_Y, width, height, 8, limits).error_code == heif_error_Ok);
+
+  size_t stride;
+  uint8_t* pixels = image->get_channel_memory(heif_channel_Y, &stride);
+  pixels[(height - 1) * stride + width - 1] = 0x5a;
+
+  Error err = image->extend_padding_to_size(requested_width, requested_height, false, limits);
+  REQUIRE(err.error_code == heif_error_Usage_error);
+  REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+  REQUIRE(image->get_width() == width);
+  REQUIRE(image->get_height() == height);
+  REQUIRE(image->get_width(heif_channel_Y) == width);
+  REQUIRE(image->get_height(heif_channel_Y) == height);
+  REQUIRE(pixels[(height - 1) * stride + width - 1] == 0x5a);
+}
+
+
+TEST_CASE("extend_padding_to_size rejects a smaller height even when the width grows") {
+  check_smaller_padding_request(64, 128, 128, 64);
+}
+
+
+TEST_CASE("extend_padding_to_size rejects a smaller width even when the height grows") {
+  check_smaller_padding_request(128, 64, 64, 128);
+}
+
+
+TEST_CASE("heif_image_extend_padding_to_size rejects negative dimensions") {
+  heif_image* image = nullptr;
+  REQUIRE(heif_image_create(64, 64, heif_colorspace_monochrome,
+                            heif_chroma_monochrome, &image).code == heif_error_Ok);
+  REQUIRE(heif_image_add_plane(image, heif_channel_Y, 64, 64, 8).code == heif_error_Ok);
+
+  heif_error err = heif_image_extend_padding_to_size(image, -1, 64);
+  REQUIRE(err.code == heif_error_Usage_error);
+  REQUIRE(err.subcode == heif_suberror_Invalid_parameter_value);
+  heif_image_release(image);
+}
+
+
+TEST_CASE("extend_padding_to_size grows padding without changing the visible plane") {
+  auto* limits = heif_get_global_security_limits();
+  auto image = std::make_shared<HeifPixelImage>();
+  image->create(64, 64, heif_colorspace_monochrome, heif_chroma_monochrome);
+  REQUIRE(image->add_channel(heif_channel_Y, 64, 64, 8, limits).error_code == heif_error_Ok);
+
+  size_t stride;
+  uint8_t* pixels = image->get_channel_memory(heif_channel_Y, &stride);
+  for (uint32_t y = 0; y < 64; y++) {
+    for (uint32_t x = 0; x < 64; x++) {
+      pixels[y * stride + x] = static_cast<uint8_t>(x + y);
+    }
+  }
+
+  REQUIRE(image->extend_padding_to_size(128, 64, false, limits).error_code == heif_error_Ok);
+  REQUIRE(image->get_width(heif_channel_Y) == 64);
+  REQUIRE(image->get_height(heif_channel_Y) == 64);
+  REQUIRE(image->extend_padding_to_size(128, 128, false, limits).error_code == heif_error_Ok);
+  REQUIRE(image->get_width() == 64);
+  REQUIRE(image->get_height() == 64);
+  REQUIRE(image->get_width(heif_channel_Y) == 64);
+  REQUIRE(image->get_height(heif_channel_Y) == 64);
+
+  pixels = image->get_channel_memory(heif_channel_Y, &stride);
+  REQUIRE(stride >= 128);
+  for (uint32_t y = 0; y < 128; y++) {
+    for (uint32_t x = 0; x < 128; x++) {
+      REQUIRE(pixels[y * stride + x] == static_cast<uint8_t>(std::min(x, 63U) + std::min(y, 63U)));
+    }
+  }
+}
diff --git a/tests/sequence_mixed_frame_size.cc b/tests/sequence_mixed_frame_size.cc
new file mode 100644
index 00000000..d51bea67
--- /dev/null
+++ b/tests/sequence_mixed_frame_size.cc
@@ -0,0 +1,126 @@
+/*
+  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.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_sequences.h"
+
+#include <cstdint>
+#include <cstring>
+#include <utility>
+
+namespace {
+
+heif_image* make_frame(uint32_t width, uint32_t height)
+{
+  heif_image* image = nullptr;
+  REQUIRE(heif_image_create(width, height, heif_colorspace_YCbCr, heif_chroma_420, &image).code == heif_error_Ok);
+  REQUIRE(image != nullptr);
+
+  for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) {
+    const uint32_t plane_width = channel == heif_channel_Y ? width : width / 2;
+    const uint32_t plane_height = channel == heif_channel_Y ? height : height / 2;
+    REQUIRE(heif_image_add_plane(image, channel, plane_width, plane_height, 8).code == heif_error_Ok);
+
+    size_t stride = 0;
+    uint8_t* pixels = heif_image_get_plane2(image, channel, &stride);
+    REQUIRE(pixels != nullptr);
+    for (uint32_t y = 0; y < plane_height; y++) {
+      std::memset(pixels + y * stride, 0x80, plane_width);
+    }
+  }
+
+  heif_image_set_duration(image, 1000);
+  return image;
+}
+
+heif_error append_frame(heif_track* track, heif_encoder* encoder, uint32_t width, uint32_t height)
+{
+  heif_image* image = make_frame(width, height);
+  heif_error err = heif_track_encode_sequence_image(track, image, encoder, nullptr);
+  heif_image_release(image);
+  return err;
+}
+
+} // namespace
+
+TEST_CASE("encoders configured from the first frame reject mixed frame sizes")
+{
+  bool tested_encoder = false;
+  for (const auto& [format, encoder_name] : {
+         std::pair{heif_compression_AVC, "x264"},
+         std::pair{heif_compression_HEVC, "x265"},
+         std::pair{heif_compression_HEVC, "kvazaar"},
+         std::pair{heif_compression_VVC, "uvg266"},
+         std::pair{heif_compression_VVC, "vvenc"},
+         std::pair{heif_compression_AV1, "aom"},
+         std::pair{heif_compression_AV1, "rav1e"},
+         std::pair{heif_compression_AV1, "svt"}}) {
+    const heif_encoder_descriptor* descriptor = nullptr;
+    if (heif_get_encoder_descriptors(format, encoder_name, &descriptor, 1) == 0) {
+      continue;
+    }
+
+    tested_encoder = true;
+    INFO("encoder: " << encoder_name);
+    heif_context* context = heif_context_alloc();
+    REQUIRE(context != nullptr);
+
+    heif_encoder* encoder = nullptr;
+    REQUIRE(heif_context_get_encoder(context, descriptor, &encoder).code == heif_error_Ok);
+    REQUIRE(encoder != nullptr);
+
+    heif_track* track = nullptr;
+    REQUIRE(heif_context_add_visual_sequence_track(context, 128, 64, heif_track_type_video,
+                                                   nullptr, nullptr, &track).code == heif_error_Ok);
+    REQUIRE(track != nullptr);
+
+    heif_error err = append_frame(track, encoder, 128, 64);
+    INFO("first frame: " << (err.message ? err.message : ""));
+    REQUIRE(err.code == heif_error_Ok);
+
+    for (const auto& [width, height] : {std::pair{64U, 64U}, std::pair{128U, 128U},
+                                        std::pair{64U, 128U}}) {
+      err = append_frame(track, encoder, width, height);
+      REQUIRE(err.code == heif_error_Invalid_input);
+      REQUIRE(err.message != nullptr);
+      REQUIRE(std::strstr(err.message, "size of the first frame") != nullptr);
+    }
+
+    // A rejected frame must not prevent a later frame of the correct size.
+    err = append_frame(track, encoder, 128, 64);
+    INFO("matching frame after rejection: " << (err.message ? err.message : ""));
+    REQUIRE(err.code == heif_error_Ok);
+
+    REQUIRE(heif_track_encode_end_of_sequence(track, encoder).code == heif_error_Ok);
+    heif_encoder_release(encoder);
+    heif_context_free(context);
+  }
+
+  if (!tested_encoder) {
+    SKIP("Skipping test because no fixed-size sequence encoder is available.");
+  }
+}