Allow repetition count for animated AVIF

The HEIF specification allows the use of edit lists for specifying
the number of times an animation should be repeated.

This CL does the following:
 * Adds a "--repetition-count" option to avifenc and the libavif API to
   write the correct repetition count using the edit list box as
   specified in Section 9.6 of ISO/IEC 23008-12 Part 12.
 * Adds a repetitionCount member to the avifDecoder struct for readers
   to be able to see the repetition count when decoding.
 * Adds two #defines one to specify INFINITE repetition and another one
   to specify UNKNOWN repetitions (in case of images made with older
   libavif versions prior to this CL, the repetition count will be
   deduced as UNKNOWN because there is no EditList box). Applications
   can choose to handle the UNKNOWN case however they want.

Compatibility check for the generated files:
 * Compliance Warden does not report any errors.
 * Chrome is able to display the files (ignoring the repetition count).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76d2007..c7acf22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,12 +7,15 @@
 ## [Unreleased]
 
 There are incompatible ABI changes in this release. The clli member was added
-to the avifImage struct.
+to the avifImage struct. The repetitionCount member was added to the avifEncoder
+and avifDecoder structs.
 
 ### Added
 * Add STATIC library target avif_internal to allow tests to access functions
   from internal.h when BUILD_SHARED_LIBS is ON.
 * Add clli metadata read and write support
+* repetitionCount member added to avifEncoder and avifDecoder struct to specify
+  the number of repetitions for animated image sequences.
 
 ### Changed
 * Exif and XMP metadata is exported to PNG and JPEG files by default,
diff --git a/apps/avifenc.c b/apps/avifenc.c
index ea5ca4b..81765a0 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -108,6 +108,7 @@
     printf("    --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in num/denom pairs)\n");
     printf("    --irot ANGLE                      : Add irot property (rotation). [0-3], makes (90 * ANGLE) degree rotation anti-clockwise\n");
     printf("    --imir MODE                       : Add imir property (mirroring). 0=top-to-bottom, 1=left-to-right\n");
+    printf("    --repetition-count N or infinite  : Number of times an animated image sequence will be repeated. Use 'infinite' for infinite repetitions (Default: infinite)\n");
     printf("    --                                : Signals the end of options. Everything after this is interpreted as file names.\n");
     printf("\n");
     if (avifCodecName(AVIF_CODEC_CHOICE_AOM, 0)) {
@@ -461,6 +462,7 @@
     avifBool cropConversionRequired = AVIF_FALSE;
     uint8_t irotAngle = 0xff; // sentinel value indicating "unused"
     uint8_t imirMode = 0xff;  // sentinel value indicating "unused"
+    int repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
     avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO;
     avifRange requestedRange = AVIF_RANGE_FULL;
     avifBool lossless = AVIF_FALSE;
@@ -784,6 +786,18 @@
                 returnCode = 1;
                 goto cleanup;
             }
+        } else if (!strcmp(arg, "--repetition-count")) {
+            NEXTARG();
+            if (!strcmp(arg, "infinite")) {
+                repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
+            } else {
+                repetitionCount = atoi(arg);
+                if (repetitionCount < 0) {
+                    fprintf(stderr, "ERROR: Invalid repetition count: %s\n", arg);
+                    returnCode = 1;
+                    goto cleanup;
+                }
+            }
         } else if (!strcmp(arg, "-l") || !strcmp(arg, "--lossless")) {
             lossless = AVIF_TRUE;
         } else if (!strcmp(arg, "-p") || !strcmp(arg, "--premultiply")) {
@@ -1205,7 +1219,9 @@
     encoder->speed = speed;
     encoder->timescale = outputTiming.timescale;
     encoder->keyframeInterval = keyframeInterval;
+    encoder->repetitionCount = repetitionCount;
 
+    avifBool isImageSequence = AVIF_FALSE;
     if (gridDimsCount > 0) {
         avifResult addImageResult =
             avifEncoderAddImageGrid(encoder, gridDims[0], gridDims[1], (const avifImage * const *)gridCells, AVIF_ADD_IMAGE_FLAG_SINGLE);
@@ -1319,6 +1335,9 @@
                 goto cleanup;
             }
         }
+        if (nextImageIndex > 0) {
+            isImageSequence = AVIF_TRUE;
+        }
     }
 
     avifResult finishResult = avifEncoderFinish(encoder, &raw);
@@ -1331,6 +1350,13 @@
     printf("Encoded successfully.\n");
     printf(" * Color AV1 total size: %" AVIF_FMT_ZU " bytes\n", encoder->ioStats.colorOBUSize);
     printf(" * Alpha AV1 total size: %" AVIF_FMT_ZU " bytes\n", encoder->ioStats.alphaOBUSize);
+    if (isImageSequence) {
+        if (encoder->repetitionCount == AVIF_REPETITION_COUNT_INFINITE) {
+            printf(" * Repetition Count: Infinite\n");
+        } else {
+            printf(" * Repetition Count: %d\n", encoder->repetitionCount);
+        }
+    }
     FILE * f = fopen(outputFilename, "wb");
     if (!f) {
         fprintf(stderr, "ERROR: Failed to open file for write: %s\n", outputFilename);
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 4fb1736..a986ed6 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -135,6 +135,15 @@
 void avifContainerDump(const avifDecoder * decoder)
 {
     avifImageDumpInternal(decoder->image, 0, 0, decoder->alphaPresent, decoder->progressiveState);
+    if (decoder->imageCount > 1) {
+        if (decoder->repetitionCount == AVIF_REPETITION_COUNT_INFINITE) {
+            printf(" * Repeat Count   : Infinite\n");
+        } else if (decoder->repetitionCount == AVIF_REPETITION_COUNT_UNKNOWN) {
+            printf(" * Repeat Count   : Unknown\n");
+        } else {
+            printf(" * Repeat Count   : %d\n", decoder->repetitionCount);
+        }
+    }
 }
 
 void avifPrintVersions(void)
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 99a06a2..b91a9fa 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -88,6 +88,12 @@
 #define AVIF_SPEED_SLOWEST 0
 #define AVIF_SPEED_FASTEST 10
 
+// This value is used to indicate that an animated AVIF file has to be repeated infinitely.
+#define AVIF_REPETITION_COUNT_INFINITE -1
+// This value is used if an animated AVIF file does not have repetitions specified using an EditList box. Applications can choose
+// to handle this case however they want.
+#define AVIF_REPETITION_COUNT_UNKNOWN -2
+
 typedef enum avifPlanesFlag
 {
     AVIF_PLANES_YUV = (1 << 0),
@@ -931,8 +937,13 @@
     avifProgressiveState progressiveState; // See avifProgressiveState declaration
     avifImageTiming imageTiming;           //
     uint64_t timescale;                    // timescale of the media (Hz)
-    double duration;                       // in seconds (durationInTimescales / timescale)
-    uint64_t durationInTimescales;         // duration in "timescales"
+    double duration;                       // duration of a single playback of the image sequence in seconds
+                                           // (durationInTimescales / timescale)
+    uint64_t durationInTimescales;         // duration of a single playback of the image sequence in "timescales"
+    int repetitionCount;                   // number of times the sequence has to be repeated. This can also be one of
+                                           // AVIF_REPETITION_COUNT_INFINITE or AVIF_REPETITION_COUNT_UNKNOWN. Essentially, if
+                                           // repetitionCount is a non-negative integer `n`, then the image sequence should be
+                                           // played back `n + 1` times.
 
     // This is true when avifDecoderParse() detects an alpha plane. Use this to find out if alpha is
     // present after a successful call to avifDecoderParse(), but prior to any call to
@@ -1076,6 +1087,11 @@
     int speed;
     int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default).
     uint64_t timescale;   // timescale of the media (Hz)
+    int repetitionCount;  // Number of times the image sequence should be repeated. This can also be set to
+                          // AVIF_REPETITION_COUNT_INFINITE for infinite repetitions.  Only applicable for image sequences.
+                          // Essentially, if repetitionCount is a non-negative integer `n`, then the image sequence should be
+                          // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE.
+
     // changeable encoder settings
     int minQuantizer;
     int maxQuantizer;
diff --git a/include/avif/internal.h b/include/avif/internal.h
index baf1124..90f4daa 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -473,6 +473,9 @@
 } avifSequenceHeader;
 avifBool avifSequenceHeaderParse(avifSequenceHeader * header, const avifROData * sample);
 
+#define AVIF_UNKNOWN_DURATION64 UINT64_MAX
+#define AVIF_UNKNOWN_DURATION32 UINT32_MAX
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
diff --git a/src/read.c b/src/read.c
index 4dc7cd3..bdfff92 100644
--- a/src/read.c
+++ b/src/read.c
@@ -351,6 +351,10 @@
     uint32_t premByID; // if non-zero, this track is premultiplied by Track #{premByID}
     uint32_t mediaTimescale;
     uint64_t mediaDuration;
+    uint64_t trackDuration;
+    uint64_t segmentDuration;
+    avifBool isRepeating;
+    int repetitionCount;
     uint32_t width;
     uint32_t height;
     avifSampleTable * sampleTable;
@@ -2456,17 +2460,19 @@
     uint32_t ignored32, trackID;
     uint64_t ignored64;
     if (version == 1) {
-        AVIF_CHECK(avifROStreamReadU64(&s, &ignored64)); // unsigned int(64) creation_time;
-        AVIF_CHECK(avifROStreamReadU64(&s, &ignored64)); // unsigned int(64) modification_time;
-        AVIF_CHECK(avifROStreamReadU32(&s, &trackID));   // unsigned int(32) track_ID;
-        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32)); // const unsigned int(32) reserved = 0;
-        AVIF_CHECK(avifROStreamReadU64(&s, &ignored64)); // unsigned int(64) duration;
+        AVIF_CHECK(avifROStreamReadU64(&s, &ignored64));            // unsigned int(64) creation_time;
+        AVIF_CHECK(avifROStreamReadU64(&s, &ignored64));            // unsigned int(64) modification_time;
+        AVIF_CHECK(avifROStreamReadU32(&s, &trackID));              // unsigned int(32) track_ID;
+        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32));            // const unsigned int(32) reserved = 0;
+        AVIF_CHECK(avifROStreamReadU64(&s, &track->trackDuration)); // unsigned int(64) duration;
     } else if (version == 0) {
-        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32)); // unsigned int(32) creation_time;
-        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32)); // unsigned int(32) modification_time;
-        AVIF_CHECK(avifROStreamReadU32(&s, &trackID));   // unsigned int(32) track_ID;
-        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32)); // const unsigned int(32) reserved = 0;
-        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32)); // unsigned int(32) duration;
+        uint32_t trackDuration;
+        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32));     // unsigned int(32) creation_time;
+        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32));     // unsigned int(32) modification_time;
+        AVIF_CHECK(avifROStreamReadU32(&s, &trackID));       // unsigned int(32) track_ID;
+        AVIF_CHECK(avifROStreamReadU32(&s, &ignored32));     // const unsigned int(32) reserved = 0;
+        AVIF_CHECK(avifROStreamReadU32(&s, &trackDuration)); // unsigned int(32) duration;
+        track->trackDuration = (trackDuration == AVIF_UNKNOWN_DURATION32) ? AVIF_UNKNOWN_DURATION64 : trackDuration;
     } else {
         // Unsupported version
         avifDiagnosticsPrintf(diag, "Box[tkhd] has an unsupported version [%u]", version);
@@ -2784,12 +2790,70 @@
     return AVIF_TRUE;
 }
 
+static avifBool avifParseEditListBox(avifTrack * track, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag)
+{
+    BEGIN_STREAM(s, raw, rawLen, diag, "Box[elst]");
+
+    uint8_t version;
+    uint32_t flags;
+    AVIF_CHECK(avifROStreamReadVersionAndFlags(&s, &version, &flags));
+
+    if ((flags & 1) == 0) {
+        track->isRepeating = AVIF_FALSE;
+        return AVIF_TRUE;
+    }
+
+    track->isRepeating = AVIF_TRUE;
+    uint32_t entry_count;
+    avifROStreamReadU32(&s, &entry_count); // unsigned int(32) entry_count;
+    if (entry_count > 1) {
+        avifDiagnosticsPrintf(diag, "Box[elst] contains an entry_count > 1 [%d]", entry_count);
+        return AVIF_FALSE;
+    }
+
+    if (version == 1) {
+        avifROStreamReadU64(&s, &track->segmentDuration); // unsigned int(64) segment_duration;
+    } else {
+        uint32_t segmentDuration;
+        avifROStreamReadU32(&s, &segmentDuration); // unsigned int(32) segment_duration;
+        track->segmentDuration = segmentDuration;
+    }
+    if (track->segmentDuration == 0) {
+        avifDiagnosticsPrintf(diag, "Box[elst] Invalid value for segment_duration (0).");
+        return AVIF_FALSE;
+    }
+    return AVIF_TRUE;
+}
+
+static avifBool avifParseEditBox(avifTrack * track, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag)
+{
+    BEGIN_STREAM(s, raw, rawLen, diag, "Box[edts]");
+
+    avifBool elstBoxSeen = AVIF_FALSE;
+    while (avifROStreamHasBytesLeft(&s, 1)) {
+        avifBoxHeader header;
+        AVIF_CHECK(avifROStreamReadBoxHeader(&s, &header));
+
+        if (!memcmp(header.type, "elst", 4)) {
+            if (elstBoxSeen) {
+                avifDiagnosticsPrintf(diag, "More than one [elst] Box was found.");
+                return AVIF_FALSE;
+            }
+            AVIF_CHECK(avifParseEditListBox(track, avifROStreamCurrent(&s), header.size, diag));
+            elstBoxSeen = AVIF_TRUE;
+        }
+        AVIF_CHECK(avifROStreamSkip(&s, header.size));
+    }
+    return AVIF_TRUE;
+}
+
 static avifBool avifParseTrackBox(avifDecoderData * data, uint64_t rawOffset, const uint8_t * raw, size_t rawLen, uint32_t imageSizeLimit, uint32_t imageDimensionLimit)
 {
     BEGIN_STREAM(s, raw, rawLen, data->diag, "Box[trak]");
 
     avifTrack * track = avifDecoderDataCreateTrack(data);
 
+    avifBool edtsBoxSeen = AVIF_FALSE;
     while (avifROStreamHasBytesLeft(&s, 1)) {
         avifBoxHeader header;
         AVIF_CHECK(avifROStreamReadBoxHeader(&s, &header));
@@ -2802,10 +2866,52 @@
             AVIF_CHECK(avifParseMediaBox(track, rawOffset + avifROStreamOffset(&s), avifROStreamCurrent(&s), header.size, data->diag));
         } else if (!memcmp(header.type, "tref", 4)) {
             AVIF_CHECK(avifTrackReferenceBox(track, avifROStreamCurrent(&s), header.size, data->diag));
+        } else if (!memcmp(header.type, "edts", 4)) {
+            if (edtsBoxSeen) {
+                avifDiagnosticsPrintf(data->diag, "More than one [edts] Box was found.");
+                return AVIF_FALSE;
+            }
+            AVIF_CHECK(avifParseEditBox(track, avifROStreamCurrent(&s), header.size, data->diag));
+            edtsBoxSeen = AVIF_TRUE;
         }
 
         AVIF_CHECK(avifROStreamSkip(&s, header.size));
     }
+    if (!edtsBoxSeen) {
+        track->repetitionCount = AVIF_REPETITION_COUNT_UNKNOWN;
+    } else if (track->isRepeating) {
+        if (track->trackDuration == AVIF_UNKNOWN_DURATION64) {
+            // If isRepeating is true and track duration is unknown, then set the repetition count to infinite (Section 9.6.1 of
+            // ISO/IEC 23008-12 Part 12).
+            track->repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
+        } else {
+            // Section 9.6.1. of ISO/IEC 23008-12 Part 12: 1, the entire edit list is repeated a sufficient number of times to
+            // equal the track duration.
+            //
+            // Since libavif uses repetitionCount (which is 0-based), we subtract the value by 1 to derive the number of
+            // repetitions.
+            assert(track->segmentDuration != 0);
+            // We specifically check for trackDuration == 0 here and not when it is actually read in order to accept files which
+            // inadvertently has a trackDuration of 0 without any edit lists.
+            if (track->trackDuration == 0) {
+                avifDiagnosticsPrintf(data->diag, "Invalid track duration 0.");
+                return AVIF_FALSE;
+            }
+            const uint64_t repetitionCount =
+                (track->trackDuration / track->segmentDuration) + (track->trackDuration % track->segmentDuration != 0) - 1;
+            if (repetitionCount > INT_MAX) {
+                // repetitionCount does not fit in an integer and hence it is
+                // likely to be a very large value. So, we just set it to
+                // infinite.
+                track->repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
+            } else {
+                track->repetitionCount = (int)repetitionCount;
+            }
+        }
+    } else {
+        track->repetitionCount = 0;
+    }
+
     return AVIF_TRUE;
 }
 
@@ -3435,6 +3541,10 @@
         } else {
             decoder->duration = 0;
         }
+        // If the alphaTrack->repetitionCount and colorTrack->repetitionCount are different, we will simply use the
+        // colorTrack's repetitionCount.
+        decoder->repetitionCount = colorTrack->repetitionCount;
+
         memset(&decoder->imageTiming, 0, sizeof(decoder->imageTiming)); // to be set in avifDecoderNextImage()
 
         decoder->image->width = colorTrack->width;
diff --git a/src/write.c b/src/write.c
index 8ab88f8..739d376 100644
--- a/src/write.c
+++ b/src/write.c
@@ -1534,10 +1534,26 @@
             /* clang-format on */
         };
 
-        uint64_t durationInTimescales = 0;
+        if (encoder->repetitionCount < 0 && encoder->repetitionCount != AVIF_REPETITION_COUNT_INFINITE) {
+            return AVIF_RESULT_INVALID_ARGUMENT;
+        }
+
+        uint64_t framesDurationInTimescales = 0;
         for (uint32_t frameIndex = 0; frameIndex < encoder->data->frames.count; ++frameIndex) {
             const avifEncoderFrame * frame = &encoder->data->frames.frame[frameIndex];
-            durationInTimescales += frame->durationInTimescales;
+            framesDurationInTimescales += frame->durationInTimescales;
+        }
+        uint64_t durationInTimescales;
+        if (encoder->repetitionCount == AVIF_REPETITION_COUNT_INFINITE) {
+            durationInTimescales = AVIF_UNKNOWN_DURATION64;
+        } else {
+            uint64_t loopCount = encoder->repetitionCount + 1;
+            assert(framesDurationInTimescales != 0);
+            if (loopCount > UINT64_MAX / framesDurationInTimescales) {
+                // The multiplication will overflow uint64_t.
+                return AVIF_RESULT_INVALID_ARGUMENT;
+            }
+            durationInTimescales = framesDurationInTimescales * loopCount;
         }
 
         // -------------------------------------------------------------------
@@ -1602,6 +1618,17 @@
                 avifRWStreamFinishBox(&s, tref);
             }
 
+            avifBoxMarker edts = avifRWStreamWriteBox(&s, "edts", AVIF_BOX_SIZE_TBD);
+            uint32_t elstFlags = (encoder->repetitionCount != 0);
+            avifBoxMarker elst = avifRWStreamWriteFullBox(&s, "elst", AVIF_BOX_SIZE_TBD, 1, elstFlags);
+            avifRWStreamWriteU32(&s, 1);                          // unsigned int(32) entry_count;
+            avifRWStreamWriteU64(&s, framesDurationInTimescales); // unsigned int(64) segment_duration;
+            avifRWStreamWriteU64(&s, 0);                          // unsigned int(64) media_time;
+            avifRWStreamWriteU16(&s, 1);                          // unsigned int(16) media_rate_integer;
+            avifRWStreamWriteU16(&s, 0);                          // unsigned int(16) media_rate_fraction = 0;
+            avifRWStreamFinishBox(&s, elst);
+            avifRWStreamFinishBox(&s, edts);
+
             if (!item->alpha) {
                 avifEncoderWriteTrackMetaBox(encoder, &s);
             }
@@ -1612,7 +1639,7 @@
             avifRWStreamWriteU64(&s, now);                          // unsigned int(64) creation_time;
             avifRWStreamWriteU64(&s, now);                          // unsigned int(64) modification_time;
             avifRWStreamWriteU32(&s, (uint32_t)encoder->timescale); // unsigned int(32) timescale;
-            avifRWStreamWriteU64(&s, durationInTimescales);         // unsigned int(64) duration;
+            avifRWStreamWriteU64(&s, framesDurationInTimescales);   // unsigned int(64) duration;
             avifRWStreamWriteU16(&s, 21956);                        // bit(1) pad = 0; unsigned int(5)[3] language; ("und")
             avifRWStreamWriteU16(&s, 0);                            // unsigned int(16) pre_defined = 0;
             avifRWStreamFinishBox(&s, mdhd);