summaryrefslogtreecommitdiff
path: root/src/video-support/AVIMetadataLoader.vala
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2023-06-14 20:36:37 +0200
committerJörg Frings-Fürst <debian@jff.email>2023-06-14 20:36:37 +0200
commitbb80d3feebdc9acc52e3f4ad24084d8425f043a2 (patch)
tree2084a84c39f159c6aea254775dc0880d52579d45 /src/video-support/AVIMetadataLoader.vala
parentb26ff0798252a1a8072dd2c7a67f6205de9fde11 (diff)
parent31804433d72460cbe0a39f9f8ea5e76058d84cda (diff)
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'src/video-support/AVIMetadataLoader.vala')
-rw-r--r--src/video-support/AVIMetadataLoader.vala227
1 files changed, 227 insertions, 0 deletions
diff --git a/src/video-support/AVIMetadataLoader.vala b/src/video-support/AVIMetadataLoader.vala
new file mode 100644
index 0000000..2b507e2
--- /dev/null
+++ b/src/video-support/AVIMetadataLoader.vala
@@ -0,0 +1,227 @@
+public class AVIMetadataLoader {
+
+ private File file = null;
+
+ // A numerical date string, i.e 2010:01:28 14:54:25
+ private const int NUMERICAL_DATE_LENGTH = 19;
+
+ // Marker for timestamp section in a Nikon nctg blob.
+ private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13;
+
+ // Size limit to ensure we don't parse forever on a bad file.
+ private const int MAX_STRD_LENGTH = 100;
+
+ public AVIMetadataLoader(File file) {
+ this.file = file;
+ }
+
+ public MetadataDateTime? get_creation_date_time() {
+ return new MetadataDateTime(get_creation_date_time_for_avi());
+ }
+
+ public string? get_title() {
+ // Not supported.
+ return null;
+ }
+
+ // Checks if the given file is an AVI file.
+ public bool is_supported() {
+ AVIChunk chunk = new AVIChunk(file);
+ bool ret = false;
+ try {
+ chunk.open_file();
+ chunk.read_chunk();
+ // Look for the header and identifier.
+ if ("RIFF" == chunk.get_current_chunk_name() &&
+ "AVI " == chunk.read_name()) {
+ ret = true;
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return ret;
+ }
+
+ // Parses a Nikon nctg tag. Based losely on avi_read_nikon() in FFmpeg.
+ private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error {
+ bool found_date = false;
+ while (chunk.section_size_remaining() > sizeof(uint16)*2) {
+ uint16 tag = chunk.read_uint16();
+ uint16 size = chunk.read_uint16();
+ if (NIKON_NCTG_TIMESTAMP_MARKER == tag) {
+ found_date = true;
+ break;
+ }
+ chunk.skip(size);
+ }
+
+ if (found_date) {
+ // Read numerical date string, example: 2010:01:28 14:54:25
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) {
+ sb.append_c((char) chunk.read_byte());
+ }
+ return sb.str;
+ }
+ return "";
+ }
+
+ // Parses a Fujifilm strd tag. Based on information from:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error {
+ chunk.skip(98); // Ignore 98-byte binary blob.
+ chunk.skip(8); // Ignore the string "FUJIFILM"
+ // Read until we find four colons, then two more chars.
+ int colons = 0;
+ int post_colons = 0;
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ // End of date is two chars past the fourth colon.
+ while (colons <= 4 && post_colons < 2) {
+ char c = (char) chunk.read_byte();
+ if (4 == colons) {
+ post_colons++;
+ }
+ if (':' == c) {
+ colons++;
+ }
+ if (c.isprint()) {
+ sb.append_c(c);
+ }
+ if (sb.len > MAX_STRD_LENGTH) {
+ return ""; // Give up searching.
+ }
+ }
+
+ if (sb.str.length < NUMERICAL_DATE_LENGTH) {
+ return "";
+ }
+ // Date is now at the end of the string.
+ return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH);
+ }
+
+ // Recursively read file until the section is found.
+ private string? read_section(AVIChunk chunk) throws GLib.Error {
+ while (true) {
+ chunk.read_chunk();
+ string name = chunk.get_current_chunk_name();
+ if ("IDIT" == name) {
+ return chunk.section_to_string();
+ } else if ("nctg" == name) {
+ return read_nikon_nctg_tag(chunk);
+ } else if ("strd" == name) {
+ return read_fuji_strd_tag(chunk);
+ }
+
+ if ("LIST" == name) {
+ chunk.read_name(); // Read past list name.
+ string result = read_section(chunk.get_first_child_chunk());
+ if (null != result) {
+ return result;
+ }
+ }
+
+ if (chunk.is_last_chunk()) {
+ break;
+ }
+ chunk.next_chunk();
+ }
+ return null;
+ }
+
+ // Parses a date from a string.
+ // Largely based on GStreamer's avi/gstavidemux.c
+ // and the information here:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private DateTime? parse_date(string sdate) {
+ if (sdate.length == 0) {
+ return null;
+ }
+
+ int year, month, day, hour, min, sec;
+ char weekday[4];
+ char monthstr[4];
+ DateTime parsed_date;
+
+ if (sdate[0].isdigit()) {
+ // Format is: 2005:08:17 11:42:43
+ // Format is: 2010/11/30/ 19:42
+ // Format is: 2010/11/30 19:42
+ string tmp = sdate.dup();
+ tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces
+ sec = 0;
+ int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec);
+ if(result < 5) {
+ return null;
+ }
+
+ parsed_date = new DateTime.utc(year, month, day, hour, min, sec);
+ } else {
+ // Format is: Mon Mar 3 09:44:56 2008
+ if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour,
+ out min, out sec, out year)) {
+ return null; // Error
+ }
+ parsed_date = new DateTime.local(year, month_from_string((string)monthstr), day, hour, min, sec);
+ }
+
+ return parsed_date;
+ }
+
+ private DateMonth month_from_string(string s) {
+ switch (s.down()) {
+ case "jan":
+ return DateMonth.JANUARY;
+ case "feb":
+ return DateMonth.FEBRUARY;
+ case "mar":
+ return DateMonth.MARCH;
+ case "apr":
+ return DateMonth.APRIL;
+ case "may":
+ return DateMonth.MAY;
+ case "jun":
+ return DateMonth.JUNE;
+ case "jul":
+ return DateMonth.JULY;
+ case "aug":
+ return DateMonth.AUGUST;
+ case "sep":
+ return DateMonth.SEPTEMBER;
+ case "oct":
+ return DateMonth.OCTOBER;
+ case "nov":
+ return DateMonth.NOVEMBER;
+ case "dec":
+ return DateMonth.DECEMBER;
+ }
+ return DateMonth.BAD_MONTH;
+ }
+
+ private DateTime? get_creation_date_time_for_avi() {
+ AVIChunk chunk = new AVIChunk(file);
+ DateTime? timestamp = null;
+ try {
+ chunk.open_file();
+ chunk.nonsection_skip(12); // Advance past 12 byte header.
+ string sdate = read_section(chunk);
+ if (null != sdate) {
+ timestamp = parse_date(sdate.strip());
+ }
+ } catch (GLib.Error e) {
+ debug("Error while reading AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return timestamp;
+ }
+}