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