Skip to content

Creating and Decoding Animated HEIF/AVIF Sequences

Both HEIF and AVIF formats support image sequence tracks (msf1, avis), allowing animations with full 10-bit color, alpha transparency, and variable frame rates (VFR) at a fraction of the file size of traditional animated GIFs or APNGs.

This tutorial demonstrates: 1. Encoding an animated image sequence with custom per-frame durations. 2. Decoding an animated sequence frame-by-frame. 3. Converting an animated sequence to an animated GIF using Pillow.


1. Encoding an Animated Sequence

To create an animated sequence, create a visual sequence track on the HeifContext, encode successive frames, and set per-frame durations:

import numpy as np
import pylibheif

width, height = 320, 240
num_frames = 30
fps = 24
frame_duration_ms = int(1000 / fps)  # ~41ms per frame

# 1. Initialize context & AV1 sequence encoder
ctx = pylibheif.HeifContext()
encoder = pylibheif.HeifEncoder(pylibheif.HeifCompressionFormat.AV1)
encoder.apply_preset("fast")
encoder.quality = 75

# 2. Add visual sequence track
track = ctx.add_visual_sequence_track(
    width=width,
    height=height,
    track_type=pylibheif.HeifTrackType.ImageSequence
)

# 3. Generate and append animation frames
for i in range(num_frames):
    # Create animated bouncing circle or graphic
    frame = pylibheif.HeifImage(
        pylibheif.HeifColorspace.RGB,
        pylibheif.HeifChroma.InterleavedRGB24,
        width,
        height
    )
    plane = frame.add_plane(pylibheif.HeifChannel.Interleaved, width, height, 8)

    # Set per-frame duration in milliseconds
    frame.set_duration(frame_duration_ms)

    # Encode frame into the track
    track.encode_sequence_image(frame, encoder)

# 4. Finalize sequence and write to disk
track.encode_end_of_sequence(encoder)
ctx.write_to_file("animation.avif")
print(f"Created animated AVIF with {num_frames} frames!")

2. Decoding Sequence Frames & Durations

To read an animated container frame-by-frame:

import pylibheif

ctx = pylibheif.HeifContext()
ctx.read_from_file("animation.avif")

# Check if the container contains a sequence track
if not ctx.has_sequence():
    print("This file is a still image, not an animation sequence.")
else:
    # Retrieve the primary track
    track = ctx.get_track(0)
    print(f"Track ID: {track.id}, Width: {track.width}, Height: {track.height}")

    frame_count = 0
    total_duration_ms = 0

    while True:
        try:
            # Decode next frame into RGB24
            image = track.decode_next_image(
                pylibheif.HeifColorspace.RGB,
                pylibheif.HeifChroma.InterleavedRGB24
            )
            duration = image.get_duration()
            total_duration_ms += duration
            frame_count += 1
            print(f"Decoded Frame {frame_count}: duration = {duration} ms")
        except pylibheif.HeifError as e:
            # End_of_sequence error signals animation loop termination
            break

    print(f"Total animation length: {total_duration_ms / 1000.0:.2f} seconds ({frame_count} frames)")

3. Converting Animated AVIF to Animated GIF

You can bridge sequence frames directly into Pillow to export an animated GIF:

from PIL import Image
import pylibheif

ctx = pylibheif.HeifContext()
ctx.read_from_file("animation.avif")
track = ctx.get_track(0)

pil_frames = []
durations = []

while True:
    try:
        frame = track.decode_next_image(
            pylibheif.HeifColorspace.RGB,
            pylibheif.HeifChroma.InterleavedRGB24
        )
        pil_img = pylibheif.to_pillow(frame)
        pil_frames.append(pil_img)
        durations.append(frame.get_duration())
    except pylibheif.HeifError:
        break

if pil_frames:
    pil_frames[0].save(
        "converted_animation.gif",
        save_all=True,
        append_images=pil_frames[1:],
        duration=durations,
        loop=0
    )
    print("Successfully converted sequence to animated GIF!")