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