Add creationTime & modificationTime to avifEncoder

Allow specifying the creation_time and modification_time fields in boxes
such as mvhd for generating deterministic outputs when encoding images
sequences. Set to 0 for the current behavior of using the current time.
diff --git a/apps/avifenc.c b/apps/avifenc.c
index d183120..fefac66 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -41,6 +41,8 @@
     int layers;
     int speed;
     avifHeaderFormatFlags headerFormat;
+    uint64_t creationTime;
+    uint64_t modificationTime;
 
     avifBool paspPresent;
     uint32_t paspValues[2];
@@ -244,6 +246,8 @@
     printf("    --icc FILENAME                    : Provide an ICC profile payload to be associated with the primary item (implies --ignore-icc)\n");
     printf("    --timescale,--fps V               : Timescale for image sequences. If all frames are 1 timescale in length, this is equivalent to frames per second. (Default: 30)\n");
     printf("                                        If neither duration nor timescale are set, avifenc will attempt to use the framerate stored in a y4m header, if present.\n");
+    printf("    --creation-time                   : Creation time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the modification time)\n");
+    printf("    --modification-time               : Modification time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the current time)\n");
     printf("    -k,--keyframe INTERVAL            : Maximum keyframe interval for image sequences (any set of INTERVAL consecutive frames will have at least one keyframe). Set to 0 to disable (default).\n");
     printf("    --ignore-exif                     : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n");
     printf("    --ignore-xmp                      : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n");
@@ -1174,6 +1178,8 @@
     encoder->keyframeInterval = settings->keyframeInterval;
     encoder->repetitionCount = settings->repetitionCount;
     encoder->headerFormat = settings->headerFormat;
+    encoder->creationTime = settings->creationTime;
+    encoder->modificationTime = settings->modificationTime;
     encoder->extraLayerCount = settings->layers - 1;
     if (!avifEncodeUpdateEncoderSettings(encoder, &firstFile->settings)) {
         goto cleanup;
@@ -1475,6 +1481,8 @@
     settings.layers = 0;
     settings.speed = 6;
     settings.headerFormat = AVIF_HEADER_DEFAULT;
+    settings.creationTime = 0;
+    settings.modificationTime = 0;
     settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
     settings.keyframeInterval = 0;
     settings.ignoreExif = AVIF_FALSE;
@@ -1899,6 +1907,22 @@
                 goto cleanup;
             }
             settings.outputTiming.timescale = (uint64_t)timescaleInt;
+        } else if (!strcmp(arg, "--creation-time")) {
+            NEXTARG();
+            long long creationTime = strtoll(arg, NULL, 0);
+            if (creationTime < 0 || (unsigned long long)creationTime > UINT64_MAX) {
+                fprintf(stderr, "ERROR: Invalid creation time: %lld\n", creationTime);
+                goto cleanup;
+            }
+            settings.creationTime = (uint64_t)creationTime;
+        } else if (!strcmp(arg, "--modification-time")) {
+            NEXTARG();
+            long long modificationTime = strtoll(arg, NULL, 0);
+            if (modificationTime < 0 || (unsigned long long)modificationTime > UINT64_MAX) {
+                fprintf(stderr, "ERROR: Invalid modification time: %lld\n", modificationTime);
+                goto cleanup;
+            }
+            settings.modificationTime = (uint64_t)modificationTime;
         } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) {
             NEXTARG();
             settings.codecChoice = avifCodecChoiceFromName(arg);
diff --git a/include/avif/avif.h b/include/avif/avif.h
index f1224c0..35764b4 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -1603,6 +1603,15 @@
     // Version 1.2.0 ends here. Add any new members after this line.
     // --------------------------------------------------------------------------------------------
 
+    // Only used when encoding an image sequence (animated image). In seconds since midnight,
+    // Jan. 1, 1970 UTC (the Unix epoch). If set to 0 (the default), libavif sets the creation time
+    // to the modification time.
+    uint64_t creationTime;
+    // Only used when encoding an image sequence (animated image). In seconds since midnight,
+    // Jan. 1, 1970 UTC (the Unix epoch). If set to 0 (the default), libavif sets the modification
+    // time to the current time.
+    uint64_t modificationTime;
+
 #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
     // Perform extra steps at encoding and decoding to extend AV1 features using bundled additional image items.
     avifSampleTransformRecipe sampleTransformRecipe; // Changeable encoder setting.
diff --git a/src/write.c b/src/write.c
index b8b106a..c822a83 100644
--- a/src/write.c
+++ b/src/write.c
@@ -494,6 +494,8 @@
         return NULL;
     }
     encoder->headerFormat = AVIF_HEADER_DEFAULT;
+    encoder->creationTime = 0;
+    encoder->modificationTime = 0;
 #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
     encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_NONE;
 #endif
@@ -3219,10 +3221,14 @@
 #endif // AVIF_ENABLE_EXPERIMENTAL_MINI
 
     const avifImage * imageMetadata = encoder->data->imageMetadata;
+    uint64_t now = (uint64_t)time(NULL);
+    uint64_t modificationTime = (encoder->modificationTime != 0) ? encoder->modificationTime : now;
+    uint64_t creationTime = (encoder->creationTime != 0) ? encoder->creationTime : modificationTime;
     // The epoch for creation_time and modification_time is midnight, Jan. 1,
     // 1904, in UTC time. Add the number of seconds between that epoch and the
     // Unix epoch.
-    uint64_t now = (uint64_t)time(NULL) + 2082844800;
+    creationTime += 2082844800;
+    modificationTime += 2082844800;
 
     avifRWStream s;
     avifRWStreamStart(&s, output);
@@ -3586,8 +3592,8 @@
 
         avifBoxMarker mvhd;
         AVIF_CHECKRES(avifRWStreamWriteFullBox(&s, "mvhd", AVIF_BOX_SIZE_TBD, 1, 0, &mvhd));
-        AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                          // unsigned int(64) creation_time;
-        AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                          // unsigned int(64) modification_time;
+        AVIF_CHECKRES(avifRWStreamWriteU64(&s, creationTime));                 // unsigned int(64) creation_time;
+        AVIF_CHECKRES(avifRWStreamWriteU64(&s, modificationTime));             // unsigned int(64) modification_time;
         AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)encoder->timescale)); // unsigned int(32) timescale;
         AVIF_CHECKRES(avifRWStreamWriteU64(&s, durationInTimescales));         // unsigned int(64) duration;
         AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0x00010000)); // template int(32) rate = 0x00010000; // typically 1.0
@@ -3621,8 +3627,8 @@
 
             avifBoxMarker tkhd;
             AVIF_CHECKRES(avifRWStreamWriteFullBox(&s, "tkhd", AVIF_BOX_SIZE_TBD, 1, 1, &tkhd));
-            AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                    // unsigned int(64) creation_time;
-            AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                    // unsigned int(64) modification_time;
+            AVIF_CHECKRES(avifRWStreamWriteU64(&s, creationTime));           // unsigned int(64) creation_time;
+            AVIF_CHECKRES(avifRWStreamWriteU64(&s, modificationTime));       // unsigned int(64) modification_time;
             AVIF_CHECKRES(avifRWStreamWriteU32(&s, itemIndex + 1));          // unsigned int(32) track_ID;
             AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0));                      // const unsigned int(32) reserved = 0;
             AVIF_CHECKRES(avifRWStreamWriteU64(&s, durationInTimescales));   // unsigned int(64) duration;
@@ -3668,8 +3674,8 @@
 
             avifBoxMarker mdhd;
             AVIF_CHECKRES(avifRWStreamWriteFullBox(&s, "mdhd", AVIF_BOX_SIZE_TBD, 1, 0, &mdhd));
-            AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                          // unsigned int(64) creation_time;
-            AVIF_CHECKRES(avifRWStreamWriteU64(&s, now));                          // unsigned int(64) modification_time;
+            AVIF_CHECKRES(avifRWStreamWriteU64(&s, creationTime));                 // unsigned int(64) creation_time;
+            AVIF_CHECKRES(avifRWStreamWriteU64(&s, modificationTime));             // unsigned int(64) modification_time;
             AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)encoder->timescale)); // unsigned int(32) timescale;
             AVIF_CHECKRES(avifRWStreamWriteU64(&s, framesDurationInTimescales));   // unsigned int(64) duration;
             AVIF_CHECKRES(avifRWStreamWriteU16(&s, 21956)); // bit(1) pad = 0; unsigned int(5)[3] language; ("und")
diff --git a/tests/test_cmd_stdin.sh b/tests/test_cmd_stdin.sh
index f00401e..af581be 100755
--- a/tests/test_cmd_stdin.sh
+++ b/tests/test_cmd_stdin.sh
@@ -32,64 +32,41 @@
 }
 trap cleanup EXIT
 
-strip_header_if() {
-  FILE="$1"
-  STRIP_HEADER="$2"
-
-  if ${STRIP_HEADER}; then
-    MDAT_OFFSET=$(GREP_OPTIONS="" LC_ALL=C \
-      grep -b -m 1 -o --text mdat "${FILE}" | cut -d: -f 1)
-    dd if="${FILE}" of="${FILE}.strip" bs=1 skip="${MDAT_OFFSET}"
-    mv "${FILE}.strip" "${FILE}"
-  fi
-}
-
 test_stdin() {
   INPUT="$1"
-  STRIP_HEADER="$2"
+  INPUT_FORMAT="$2"
   shift 2
   EXTRA_FLAGS=$@
 
   # Make sure that --stdin can be replaced with a file path and that it leads to
   # the same encoded bytes.
 
-  "${AVIFENC}" -s 8 -o "${ENCODED_FILE_REGULAR}" "${INPUT}"
-  "${AVIFENC}" -s 8 -o "${ENCODED_FILE_STDIN}" --stdin ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_REGULAR}" ${STRIP_HEADER}
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 8 -o "${ENCODED_FILE_REGULAR}" ${EXTRA_FLAGS} "${INPUT}"
+  "${AVIFENC}" -s 8 -o "${ENCODED_FILE_STDIN}" --stdin --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
 
-  "${AVIFENC}" -s 9 "${INPUT}" -o "${ENCODED_FILE_REGULAR}"
-  "${AVIFENC}" -s 9 --stdin -o "${ENCODED_FILE_STDIN}" ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_REGULAR}" ${STRIP_HEADER}
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 9 "${INPUT}" -o "${ENCODED_FILE_REGULAR}" ${EXTRA_FLAGS}
+  "${AVIFENC}" -s 9 --stdin -o "${ENCODED_FILE_STDIN}" --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
 
-  "${AVIFENC}" -s 10 "${INPUT}" "${ENCODED_FILE_REGULAR}"
-  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_REGULAR}" ${STRIP_HEADER}
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 10 ${EXTRA_FLAGS} "${INPUT}" "${ENCODED_FILE_REGULAR}"
+  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
-  "${AVIFENC}" -s 10 "${ENCODED_FILE_STDIN}" --stdin ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 10 "${ENCODED_FILE_STDIN}" --stdin --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
 
-  "${AVIFENC}" -s 10 "${INPUT}" -q 90 "${ENCODED_FILE_REGULAR}"
-  "${AVIFENC}" -s 10 --stdin -q 90 "${ENCODED_FILE_STDIN}" ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_REGULAR}" ${STRIP_HEADER}
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 10 "${INPUT}" -q 90 ${EXTRA_FLAGS} "${ENCODED_FILE_REGULAR}"
+  "${AVIFENC}" -s 10 --stdin -q 90 "${ENCODED_FILE_STDIN}" --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
-  "${AVIFENC}" -s 10 "${ENCODED_FILE_STDIN}" -q 90 --stdin ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 10 "${ENCODED_FILE_STDIN}" -q 90 --stdin --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
-  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" -q 90 ${EXTRA_FLAGS} < "${INPUT}"
-  strip_header_if "${ENCODED_FILE_STDIN}" ${STRIP_HEADER}
+  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" -q 90 --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}"
 
   # Negative tests.
 
   # WARNING: Trailing options with update suffix has no effect. Place them before the input you intend to apply to.
-  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" -q:u 90 ${EXTRA_FLAGS} < "${INPUT}"
+  "${AVIFENC}" -s 10 --stdin "${ENCODED_FILE_STDIN}" -q:u 90 --input-format ${INPUT_FORMAT} ${EXTRA_FLAGS} < "${INPUT}"
   cmp --silent "${ENCODED_FILE_REGULAR}" "${ENCODED_FILE_STDIN}" && exit 1
 
   # ERROR: there cannot be any other input if --stdin is specified
@@ -102,14 +79,14 @@
 }
 
 pushd ${TMP_DIR}
-  test_stdin "${INPUT_Y4M_STILL}" false
-  test_stdin "${INPUT_PNG}" false --input-format png
-  test_stdin "${INPUT_JPEG}" false --input-format jpeg
+  test_stdin "${INPUT_Y4M_STILL}" y4m
+  test_stdin "${INPUT_PNG}" png
+  test_stdin "${INPUT_JPEG}" jpeg
 
-  # The output of avifenc for animations is not deterministic because of boxes
-  # such as mvhd and its field creation_time. Strip the whole header to compare
-  # only the encoded samples.
-  test_stdin "${INPUT_Y4M_ANIMATED}" true
+  # Make the output of avifenc for animations deterministic by specifying the
+  # creation_time and modification_time fields in boxes such as mvhd.
+  NOW=$(date +%s)
+  test_stdin "${INPUT_Y4M_ANIMATED}" y4m --creation-time ${NOW} --modification-time ${NOW}
 popd
 
 exit 0