Commit e18c0ce8d for imagemagick.org

commit e18c0ce8d34946d2f40996f509ad97307a83abbd
Author: Madars <mad182@gmail.com>
Date:   Wed Mar 25 11:54:27 2026 +0200

    Animated AVIF support (libheif 1.20.0+) (#8640)

    * Add animated AVIF (image sequence) reading support via libheif sequence API

    Use heif_sequences.h track API (libheif >= 1.19.0) to decode animated
    AVIF files. When a HEIF container has an image sequence, frames are
    decoded from the visual track with proper per-frame delay/timing
    derived from the track timescale. Falls back to the existing top-level
    images approach for non-animated files.

    Key changes:
    - Include heif_sequences.h for libheif >= 1.19.0
    - Add ReadHEICSequenceFrames() using heif_track_decode_next_image()
    - Detect sequences with heif_context_has_sequence() before falling
      back to the primary image / top-level images path
    - Set ticks_per_second from track timescale, delay from frame duration
    - Properly propagate alpha trait and BackgroundDispose to all frames

    * Add animated AVIF sequence writing support via libheif track API

    When writing AVIF with multiple frames (adjoin mode), use the libheif
    sequence track API (>= 1.19.0) to produce proper animated AVIF output
    instead of encoding frames as separate top-level images.

    Key changes:
    - Add WriteHEICSequenceImage() that creates a visual sequence track
      and encodes frames with heif_track_encode_sequence_image()
    - Set track timescale from image->ticks_per_second and frame duration
      from image->delay via heif_track_options_set_timescale() and
      heif_image_set_duration()
    - Set sequence-level timescale with heif_context_set_sequence_timescale()
      to ensure proper duration computation in the output file
    - Route multi-frame AVIF writes through the sequence writer in
      WriteHEICImage(), falling back to the existing still-image path
      for single frames and non-AVIF formats
    - Supports quality, speed, chroma, and ICC profile options

    * Some checks and cleanup

    * Fix error handling logic in heic writer loops

    When an error occurred in heif_image_create or heif_track_encode_sequence_image
    during a multi-frame encode, the loop would break but 'status' was never set
    to MagickFalse (it retained the initialized MagickTrue value). This caused the
    encoder to proceed to write a potentially empty/corrupt file and incorrectly
    report success.

    This logic bug was present in the existing WriteHEICImage as well as the new
    WriteHEICSequenceImage. Fixed both to properly assign IsHEIFSuccess to status
    before breaking on error.

    * Auto-coalesce images dynamically on encode for APNG/AVIF sequences

    Since libheif requires every frame of a sequence encode to match the dimensions and placement expected by the track precisely, formats like GIF that use delta-frames (smaller dimensions + page offsets) caused the encoding loop to fail on the unequal frame sizes. We now detect this dynamically in WriteHEICImage and CoalesceImages if necessary to guarantee fully-baked frames before hitting WriteHEICSequenceImage.

    * Update sequence API checks to require libheif >= 1.20.0

    * Restore libheif >= 1.19.0 checks for security limits

diff --git a/coders/heic.c b/coders/heic.c
index ed1e4048d..65edbd9e1 100644
--- a/coders/heic.c
+++ b/coders/heic.c
@@ -82,6 +82,9 @@
 #if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
 #include <libheif/heif_properties.h>
 #endif
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+#include <libheif/heif_sequences.h>
+#endif
 #endif

 #if defined(MAGICKCORE_HEIC_DELEGATE)
@@ -631,6 +634,249 @@ static MagickBooleanType ReadHEICImageHandle(const ImageInfo *image_info,
   return(MagickTrue);
 }

+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+static MagickBooleanType ReadHEICSequenceFrames(const ImageInfo *image_info,
+  Image *image,struct heif_context *heif_context,ExceptionInfo *exception)
+{
+  const uint8_t
+    *p,
+    *pixels;
+
+  enum heif_channel
+    channel;
+
+  enum heif_chroma
+    chroma;
+
+  heif_track
+    *track;
+
+  int
+    bits_per_pixel,
+    has_alpha,
+    shift,
+    stride = 0;
+
+  MagickBooleanType
+    status;
+
+  size_t
+    scene;
+
+  struct heif_decoding_options
+    *decode_options;
+
+  struct heif_error
+    error;
+
+  struct heif_image
+    *heif_image;
+
+  uint16_t
+    track_width,
+    track_height;
+
+  uint32_t
+    timescale;
+
+  /*
+    Get the first visual track from the sequence.
+  */
+  track=heif_context_get_track(heif_context,0);
+  if (track == (heif_track *) NULL)
+    return(MagickFalse);
+  error=heif_track_get_image_resolution(track,&track_width,&track_height);
+  if (error.code != 0)
+    {
+      heif_track_release(track);
+      return(MagickFalse);
+    }
+  timescale=heif_track_get_timescale(track);
+  if (timescale == 0)
+    timescale=1;
+  decode_options=heif_decoding_options_alloc();
+  /*
+    Detect alpha from the track and set up chroma format.
+  */
+  has_alpha=heif_track_has_alpha_channel(track);
+  image->alpha_trait=UndefinedPixelTrait;
+  if (has_alpha != 0)
+    image->alpha_trait=BlendPixelTrait;
+  image->depth=8;
+  if (image->alpha_trait != UndefinedPixelTrait)
+    {
+      chroma=heif_chroma_interleaved_RGBA;
+      if (image->depth > 8)
+        chroma=heif_chroma_interleaved_RRGGBBAA_LE;
+    }
+  else
+    {
+      chroma=heif_chroma_interleaved_RGB;
+      if (image->depth > 8)
+        chroma=heif_chroma_interleaved_RRGGBB_LE;
+    }
+  scene=0;
+  status=MagickTrue;
+  for ( ; ; )
+  {
+    ssize_t
+      y;
+
+    uint32_t
+      duration;
+
+    if (AcquireMagickResource(ListLengthResource,scene+1) == MagickFalse)
+      {
+        status=MagickFalse;
+        break;
+      }
+    heif_image=(struct heif_image *) NULL;
+    error=heif_track_decode_next_image(track,&heif_image,heif_colorspace_RGB,
+      chroma,decode_options);
+    if (error.code == heif_error_End_of_sequence)
+      break;
+    if (error.code != 0)
+      {
+        (void) ThrowMagickException(exception,GetMagickModule(),
+          CorruptImageError,error.message,"(%d.%d) `%s'",error.code,
+          error.subcode,image->filename);
+        status=MagickFalse;
+        break;
+      }
+    /*
+      Allocate next image for frames beyond the first.
+    */
+    if (scene > 0)
+      {
+        AcquireNextImage(image_info,image,exception);
+        if (GetNextImageInList(image) == (Image *) NULL)
+          {
+            heif_image_release(heif_image);
+            status=MagickFalse;
+            break;
+          }
+        image=SyncNextImageInList(image);
+      }
+    image->scene=scene;
+    /*
+      Set frame timing: convert track timescale ticks to ticks_per_second.
+    */
+    duration=heif_image_get_duration(heif_image);
+    image->ticks_per_second=(ssize_t) timescale;
+    image->delay=(size_t) duration;
+    image->iterations=0;
+    /*
+      Set image dimensions from the decoded frame.
+    */
+    channel=heif_channel_interleaved;
+    image->columns=(size_t) heif_image_get_width(heif_image,channel);
+    image->rows=(size_t) heif_image_get_height(heif_image,channel);
+    bits_per_pixel=heif_image_get_bits_per_pixel_range(heif_image,channel);
+    if (bits_per_pixel > 0)
+      image->depth=(size_t) bits_per_pixel;
+    if (has_alpha != 0)
+      {
+        image->alpha_trait=BlendPixelTrait;
+        image->dispose=BackgroundDispose;
+      }
+    if (image_info->ping != MagickFalse)
+      {
+        heif_image_release(heif_image);
+        scene++;
+        continue;
+      }
+    if (HEICSkipImage(image_info,image) != MagickFalse)
+      {
+        heif_image_release(heif_image);
+        scene++;
+        if (image_info->number_scenes != 0)
+          if (image->scene >= (image_info->scene+image_info->number_scenes-1))
+            break;
+        continue;
+      }
+    status=SetImageExtent(image,image->columns,image->rows,exception);
+    if (status == MagickFalse)
+      {
+        heif_image_release(heif_image);
+        break;
+      }
+    pixels=heif_image_get_plane_readonly(heif_image,channel,&stride);
+    if (pixels == (const uint8_t *) NULL)
+      {
+        heif_image_release(heif_image);
+        status=MagickFalse;
+        break;
+      }
+    shift=(int) (16-image->depth);
+    if (image->depth <= 8)
+      for (y=0; y < (ssize_t) image->rows; y++)
+      {
+        Quantum
+          *q;
+
+        ssize_t
+          x;
+
+        q=QueueAuthenticPixels(image,0,y,image->columns,1,exception);
+        if (q == (Quantum *) NULL)
+          break;
+        p=pixels+(y*stride);
+        for (x=0; x < (ssize_t) image->columns; x++)
+        {
+          SetPixelRed(image,ScaleCharToQuantum((unsigned char) *(p++)),q);
+          SetPixelGreen(image,ScaleCharToQuantum((unsigned char) *(p++)),q);
+          SetPixelBlue(image,ScaleCharToQuantum((unsigned char) *(p++)),q);
+          if (image->alpha_trait != UndefinedPixelTrait)
+            SetPixelAlpha(image,ScaleCharToQuantum((unsigned char) *(p++)),q);
+          q+=(ptrdiff_t) GetPixelChannels(image);
+        }
+        if (SyncAuthenticPixels(image,exception) == MagickFalse)
+          break;
+      }
+    else
+      for (y=0; y < (ssize_t) image->rows; y++)
+      {
+        Quantum
+          *q;
+
+        ssize_t
+          x;
+
+        q=QueueAuthenticPixels(image,0,y,image->columns,1,exception);
+        if (q == (Quantum *) NULL)
+          break;
+        p=pixels+(y*stride);
+        for (x=0; x < (ssize_t) image->columns; x++)
+        {
+          unsigned short pixel = (((unsigned short) *(p+1) << 8) |
+            (*(p+0))) << shift; p+=(ptrdiff_t) 2;
+          SetPixelRed(image,ScaleShortToQuantum(pixel),q);
+          pixel=(((unsigned short) *(p+1) << 8) | (*(p+0))) << shift; p+=(ptrdiff_t) 2;
+          SetPixelGreen(image,ScaleShortToQuantum(pixel),q);
+          pixel=(((unsigned short) *(p+1) << 8) | (*(p+0))) << shift; p+=(ptrdiff_t) 2;
+          SetPixelBlue(image,ScaleShortToQuantum(pixel),q);
+          if (image->alpha_trait != UndefinedPixelTrait)
+            {
+              pixel=(((unsigned short) *(p+1) << 8) | (*(p+0))) << shift; p+=(ptrdiff_t) 2;
+              SetPixelAlpha(image,ScaleShortToQuantum(pixel),q);
+            }
+          q+=(ptrdiff_t) GetPixelChannels(image);
+        }
+        if (SyncAuthenticPixels(image,exception) == MagickFalse)
+          break;
+      }
+    heif_image_release(heif_image);
+    scene++;
+    if (image_info->number_scenes != 0)
+      if (image->scene >= (image_info->scene+image_info->number_scenes-1))
+        break;
+  }
+  heif_decoding_options_free(decode_options);
+  heif_track_release(track);
+  return(status);
+}
+#endif
+
 static void ReadHEICDepthImage(const ImageInfo *image_info,Image *image,
   struct heif_context *heif_context,struct heif_image_handle *image_handle,
   ExceptionInfo *exception)
@@ -745,6 +991,19 @@ static Image *ReadHEICImage(const ImageInfo *image_info,
       heif_context_free(heif_context);
       return(DestroyImageList(image));
     }
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+  /*
+    Check for image sequence (animated AVIF) and decode via track API.
+  */
+  if (heif_context_has_sequence(heif_context) != 0)
+    {
+      status=ReadHEICSequenceFrames(image_info,image,heif_context,exception);
+      heif_context_free(heif_context);
+      if (status == MagickFalse)
+        return(DestroyImageList(image));
+      return(GetFirstImageInList(image));
+    }
+#endif
   error=heif_context_get_primary_image_ID(heif_context,&primary_image_id);
   if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
     {
@@ -1354,6 +1613,251 @@ static MagickBooleanType WriteHEICImageRRGGBBAA(Image *image,
   return(status);
 }

+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
+  Image *image,ExceptionInfo *exception)
+{
+  const char
+    *option;
+
+  enum heif_chroma
+    chroma;
+
+  enum heif_colorspace
+    colorspace;
+
+  heif_track
+    *track = (heif_track *) NULL;
+
+  MagickBooleanType
+    lossless,
+    status;
+
+  MagickOffsetType
+    scene;
+
+  struct heif_context
+    *heif_context;
+
+  struct heif_encoder
+    *heif_encoder = (struct heif_encoder *) NULL;
+
+  struct heif_error
+    error;
+
+  struct heif_image
+    *heif_image = (struct heif_image *) NULL;
+
+  struct heif_sequence_encoding_options
+    *seq_options = (struct heif_sequence_encoding_options *) NULL;
+
+  struct heif_track_options
+    *track_options;
+
+  uint32_t
+    timescale;
+
+  /*
+    Open output image file.
+  */
+  assert(image_info != (const ImageInfo *) NULL);
+  assert(image_info->signature == MagickCoreSignature);
+  assert(image != (Image *) NULL);
+  assert(image->signature == MagickCoreSignature);
+  if (IsEventLogging() != MagickFalse)
+    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
+  status=OpenBlob(image_info,image,WriteBinaryBlobMode,exception);
+  if (status == MagickFalse)
+    return(status);
+  heif_context=heif_context_alloc();
+  if (heif_context == (struct heif_context *) NULL)
+    ThrowWriterException(ResourceLimitError,"MemoryAllocationFailed");
+  /*
+    Get encoder for AV1 (AVIF).
+  */
+  error=heif_context_get_encoder_for_format(heif_context,
+    heif_compression_AV1,&heif_encoder);
+  if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
+    {
+      heif_context_free(heif_context);
+      return(MagickFalse);
+    }
+  lossless=image_info->quality >= 100 ? MagickTrue : MagickFalse;
+  if (lossless != MagickFalse)
+    (void) heif_encoder_set_lossless(heif_encoder,1);
+  else if (image_info->quality != UndefinedCompressionQuality)
+    (void) heif_encoder_set_lossy_quality(heif_encoder,(int)
+      image_info->quality);
+  option=GetImageOption(image_info,"heic:speed");
+  if (option != (char *) NULL)
+    (void) heif_encoder_set_parameter(heif_encoder,"speed",option);
+  option=GetImageOption(image_info,"heic:chroma");
+  if (option != (char *) NULL)
+    (void) heif_encoder_set_parameter(heif_encoder,"chroma",option);
+  /*
+    Determine track timescale from the first frame.
+  */
+  if (image->ticks_per_second <= 0)
+    timescale=100;
+  else
+    timescale=(uint32_t) image->ticks_per_second;
+  heif_context_set_sequence_timescale(heif_context,timescale);
+  /*
+    Create the visual sequence track.
+  */
+  if ((image->columns > 65535) || (image->rows > 65535))
+    {
+      heif_encoder_release(heif_encoder);
+      heif_context_free(heif_context);
+      ThrowWriterException(ImageError,"WidthOrHeightExceedsLimit");
+    }
+  seq_options=heif_sequence_encoding_options_alloc();
+  if (seq_options != (struct heif_sequence_encoding_options *) NULL)
+    seq_options->save_alpha_channel=1;
+  track_options=heif_track_options_alloc();
+  if (track_options != (struct heif_track_options *) NULL)
+    heif_track_options_set_timescale(track_options,timescale);
+  error=heif_context_add_visual_sequence_track(heif_context,
+    (uint16_t) image->columns,(uint16_t) image->rows,
+    heif_track_type_image_sequence,track_options,
+    seq_options,&track);
+  if (track_options != (struct heif_track_options *) NULL)
+    heif_track_options_release(track_options);
+  if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
+    {
+      if (seq_options != (struct heif_sequence_encoding_options *) NULL)
+        heif_sequence_encoding_options_release(seq_options);
+      heif_encoder_release(heif_encoder);
+      heif_context_free(heif_context);
+      return(MagickFalse);
+    }
+  scene=0;
+  status=MagickTrue;
+  do
+  {
+    const StringInfo
+      *profile;
+
+    uint32_t
+      duration;
+
+    /*
+      Determine colorspace and chroma for this frame.
+    */
+    colorspace=heif_colorspace_YCbCr;
+    chroma=lossless != MagickFalse ? heif_chroma_444 : heif_chroma_420;
+    if ((image->alpha_trait & BlendPixelTrait) != 0)
+      {
+        if (IssRGBCompatibleColorspace(image->colorspace) == MagickFalse)
+          status=TransformImageColorspace(image,sRGBColorspace,exception);
+        colorspace=heif_colorspace_RGB;
+        chroma=heif_chroma_interleaved_RGBA;
+        if (image->depth > 8)
+          chroma=heif_chroma_interleaved_RRGGBBAA_LE;
+      }
+    else
+      if (IssRGBCompatibleColorspace(image->colorspace) != MagickFalse)
+        {
+          colorspace=heif_colorspace_RGB;
+          chroma=heif_chroma_interleaved_RGB;
+          if (image->depth > 8)
+            chroma=heif_chroma_interleaved_RRGGBB_LE;
+          if (GetPixelChannels(image) == 1)
+            {
+              colorspace=heif_colorspace_monochrome;
+              chroma=heif_chroma_monochrome;
+            }
+        }
+      else
+        if (image->colorspace != YCbCrColorspace)
+          status=TransformImageColorspace(image,YCbCrColorspace,exception);
+    if (status == MagickFalse)
+      break;
+    /*
+      Create heif_image for this frame.
+    */
+    error=heif_image_create((int) image->columns,(int) image->rows,colorspace,
+      chroma,&heif_image);
+    status=IsHEIFSuccess(image,&error,exception);
+    if (status == MagickFalse)
+      break;
+    profile=GetImageProfile(image,"icc");
+    if (profile != (StringInfo *) NULL)
+      (void) heif_image_set_raw_color_profile(heif_image,"prof",
+        GetStringInfoDatum(profile),GetStringInfoLength(profile));
+    /*
+      Fill heif_image pixels from ImageMagick image.
+    */
+    if (colorspace == heif_colorspace_YCbCr)
+      status=WriteHEICImageYCbCr(image,heif_image,exception);
+    else
+      if (image->depth > 8)
+        status=WriteHEICImageRRGGBBAA(image,heif_image,exception);
+      else
+        status=WriteHEICImageRGBA(image,heif_image,exception);
+    if (status == MagickFalse)
+      {
+        heif_image_release(heif_image);
+        heif_image=(struct heif_image *) NULL;
+        break;
+      }
+    /*
+      Set frame duration and encode into the track.
+    */
+    if (image->delay > (size_t) UINT32_MAX)
+      duration=UINT32_MAX;
+    else
+      duration=(uint32_t) image->delay;
+    if (duration == 0)
+      duration=timescale/10;
+    heif_image_set_duration(heif_image,duration);
+    error=heif_track_encode_sequence_image(track,heif_image,heif_encoder,
+      seq_options);
+    heif_image_release(heif_image);
+    heif_image=(struct heif_image *) NULL;
+    status=IsHEIFSuccess(image,&error,exception);
+    if (status == MagickFalse)
+      break;
+    if (GetNextImageInList(image) == (Image *) NULL)
+      break;
+    image=SyncNextImageInList(image);
+    status=SetImageProgress(image,SaveImagesTag,scene,
+      GetImageListLength(image));
+    if (status == MagickFalse)
+      break;
+    scene++;
+  } while (image_info->adjoin != MagickFalse);
+  /*
+    Finalize the sequence and write to output.
+  */
+  if (status != MagickFalse)
+    {
+      struct heif_writer
+        writer;
+
+      error=heif_track_encode_end_of_sequence(track,heif_encoder);
+      if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
+        status=MagickFalse;
+      if (status != MagickFalse)
+        {
+          writer.writer_api_version=1;
+          writer.write=heif_write_func;
+          error=heif_context_write(heif_context,&writer,image);
+          status=IsHEIFSuccess(image,&error,exception);
+        }
+    }
+  if (seq_options != (struct heif_sequence_encoding_options *) NULL)
+    heif_sequence_encoding_options_release(seq_options);
+  if (track != (heif_track *) NULL)
+    heif_track_release(track);
+  heif_encoder_release(heif_encoder);
+  heif_context_free(heif_context);
+  if (CloseBlob(image) == MagickFalse)
+    status=MagickFalse;
+  return(status);
+}
+#endif
+
 static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
   Image *image,ExceptionInfo *exception)
 {
@@ -1395,6 +1899,38 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
 #if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,7,0)
   encode_avif=(LocaleCompare(image_info->magick,"AVIF") == 0) ? MagickTrue :
     MagickFalse;
+#endif
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+  if ((encode_avif != MagickFalse) && (image_info->adjoin != MagickFalse) &&
+      (GetNextImageInList(image) != (Image *) NULL))
+    {
+      Image
+        *coalesce_image,
+        *next;
+
+      (void) CloseBlob(image);
+      heif_context_free(heif_context);
+
+      coalesce_image=(Image *) NULL;
+      next=GetNextImageInList(image);
+      while(next != (Image *) NULL)
+      {
+        if ((next->rows != image->rows) || (next->columns != image->columns) ||
+            (next->page.x != image->page.x) || (next->page.y != image->page.y))
+          {
+            coalesce_image=CoalesceImages(image,exception);
+            break;
+          }
+        next=GetNextImageInList(next);
+      }
+      if (coalesce_image != (Image *) NULL)
+        {
+          status=WriteHEICSequenceImage(image_info,coalesce_image,exception);
+          (void) DestroyImageList(coalesce_image);
+          return(status);
+        }
+      return(WriteHEICSequenceImage(image_info,image,exception));
+    }
 #endif
   do
   {
@@ -1465,8 +2001,6 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
     */
     error=heif_image_create((int) image->columns,(int) image->rows,colorspace,
       chroma,&heif_image);
-    if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
-      break;
     status=IsHEIFSuccess(image,&error,exception);
     if (status == MagickFalse)
       break;
@@ -1578,8 +2112,6 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
     error=heif_context_encode_image(heif_context,heif_image,heif_encoder,
       options,(struct heif_image_handle **) NULL);
     heif_encoding_options_free(options);
-    if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
-      break;
     status=IsHEIFSuccess(image,&error,exception);
     if (status == MagickFalse)
       break;