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;