diff --git a/contrib/image_time.lua b/contrib/image_time.lua new file mode 100644 index 0000000..a13a79a --- /dev/null +++ b/contrib/image_time.lua @@ -0,0 +1,540 @@ +--[[ + + image_time.lua - synchronize image time for images shot with different cameras + + Copyright (C) 2019, 2020 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + image_time - non-destructively modify the image time + + DESCRIPTION + + image_time non destructively adjusts image times by modifying the + database image exif_datetime_taken field. There are 4 modes: adjust time, + set time, synchronize time, and reset time. + + ADJUST TIME + + adjust time mode lets you chose an offset in terms of years, months, + days, hours, minutes, and seconds. The adjustment can be added or + subtracted. + + WARNING: When adding and subtracting months the result will usually + be what is expected unless the time being adjusted is at the end of + the month. This is because a month is a variable amount of time that + can be 28, 29, 30 or 31 days depending on the month. Example: It's + March 31st and I subtract a month which not sets the time to February + 31st. When that gets set to a valid time, then the date changes to + March 3rd. + + SET TIME + + set time mode allows you to pick a date and time and set the image + time accordingly. Fields may be left out. This is useful when + importing scanned images that don't have an embedded date. + + SYNCHRONIZE TIME + + I recently purchased a 7DmkII to replace my aging 7D. My 7D was still + serviceable, so I bought a remote control and figured I'd try shooting + events from 2 different perspectives. I didn't think to synchonize the + time between the 2 cameras, so when I loaded the images and sorted by + time it was a disaster. I hacked a script together with hard coded values + to adjust the exif_datetime_taken value in the database for the 7D images + so that everything sorted properly. I've tried shooting with 2 cameras + several times since that first attempt. I've gotten better at getting the + camera times close, but still haven't managed to get them to sync. So I + decided to think the problem through and write a proper script to take + care of the problem. + + RESET TIME + + Select the images and click reset. + + USAGE + + ADJUST TIME + + Change the year, month, day, hour, minute, second dropdowns to the amount + of change desired. Select add or subtract. Select the images. Click + adjust. + + SET TIME + + Set the time fields to the desired time. Select the images to change. Click + set. + + SYNCHRONIZE TIME + + Select 2 images, one from each camera, of the same moment in time. Click + the Calculate button to calculate the time difference. The difference is + displayed in the difference entry. You can manually adjust it by changing + the value if necessary. + + Select the images that need their time adjusted. Determine which way to adjust + adjust the time (add or subtract) and select the appropriate choice. + + If the image times get messed up and you just want to start over, select reset time + from the mode and reset the image times. + + RESET TIME + + Select the images and click reset. + + ADDITIONAL SOFTWARE REQUIRED + * exiv2 + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES + +]] +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local ds = require "lib/dtutils.string" +local gettext = dt.gettext + +local img_time = {} + +du.check_min_api_version("3.0.0", "image_time") + + +-- Tell gettext where to find the .mo file translating messages for a particular domain +gettext.bindtextdomain("image_time",dt.configuration.config_dir.."/lua/locale/") + +local function _(msgid) + return gettext.dgettext("image_time", msgid) +end + +local PS = dt.configuration.runnin_os == "windows" and "\\" or "/" +local ERROR = -1 + +-- function to convert from exif time to system time +local function exiftime2systime(exiftime) + local yr,mo,dy,h,m,s = string.match(exiftime, "(%d-):(%d-):(%d-) (%d-):(%d-):(%d+)") + return(os.time{year=yr, month=mo, day=dy, hour=h, min=m, sec=s}) +end + +-- function to convert from systime to exif time +local function systime2exiftime(systime) + local t = os.date("*t", systime) + return(string.format("%4d:%02d:%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.min, t.sec)) +end + +local function vars2exiftime(year, month, day, hour, min, sec) + local y = tonumber(year) and string.format("%4d", year) or " " + local mo = tonumber(month) and string.format("%02d", month) or " " + local d = tonumber(day) and string.format("%02d", day) or " " + local h = tonumber(hour) and string.format("%02d", hour) or " " + local m = tonumber(min) and string.format("%02d", min) or " " + local s = tonumber(sec) and string.format("%02d", sec) or " " + return(y .. ":" .. mo .. ":" .. d .. " " .. h .. ":" .. m .. ":" .. s) +end + +local function exiftime2vars(exiftime) + return string.match(exiftime, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)") +end + +local function calc_time_difference(image1, image2) + return math.abs(exiftime2systime(image1.exif_datetime_taken) - exiftime2systime(image2.exif_datetime_taken)) +end + +local function adjust_image_time(image, difference) + image.exif_datetime_taken = systime2exiftime(exiftime2systime(image.exif_datetime_taken) + difference) + return +end + +local function calculate_difference(images) + if #images == 2 then + img_time.diff_entry.text = calc_time_difference(images[1], images[2]) + img_time.btn.sensitive = true + else + dt.print(_("Error: 2 images must be selected")) + end +end + +local function synchronize_times(images, difference) + for _, image in ipairs(images) do + adjust_image_time(image, difference) + end +end + +local function synchronize_time(images) + local sign = 1 + if img_time.sdir.value == "subtract" then + sign = -1 + end + synchronize_times(images, tonumber(img_time.diff_entry.text) * sign) +end + +local function add_time(images) + synchronize_times(images, tonumber(img_time.diff_entry.text)) +end + +local function year_month2months(year, month) + year_months = tonumber(year) and year * 12 or 0 + local months = tonumber(month) and tonumber(month) or 0 + + return year_months + months +end + +local function months2year_month(months) + dt.print_log("months is " .. months) + local year = math.floor(months / 12) + local month = months - (year * 12) + + return year, month +end + +local function get_image_taken_time(image) + -- get original image time + local datetime = nil + + local exiv2 = df.check_if_bin_exists("exiv2") + if exiv2 then + p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. image.path .. PS .. image.filename) + if p then + for line in p:lines() do + if string.match(line, "Exif.Image.DateTime") then + datetime = string.match(line, "(%d-:%d-:%d- %d-:%d-:%d+)") + end + end + p:close() + end + else + dt.print(_("unable to detect exiv2")) + datetime = ERROR + end + return datetime +end + +local function _get_windows_image_file_creation_time(image) + local datetime = nil + local p = io.popen("dir " .. image.path .. PS .. image.filename) + if p then + for line in p:lines() do + if string.match(line, ds.sanitize_lua(image.filename)) then + local mo, day, yr, hr, min, apm = string.match(line, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)") + if apm == "PM" then + hr = hr + 12 + end + datetime = vars2exiftime(yr, mo, day, hr, min, 0) + end + end + p:close() + else + dt.print(_("unable to get information for ") .. image.filename) + datetime = ERROR + end + return datetime +end + +local function _get_nix_image_file_creation_time(image) + local datetime = nil + local p = io.popen("ls -lL --time-style=full-iso " .. image.path .. PS .. image.filename) + if p then + for line in p:lines() do + if string.match(line, ds.sanitize_lua(image.filename)) then + datetime = vars2exiftime(string.match(line, "(%d+)%-(%d-)%-(%d-) (%d-):(%d-):(%d+).")) + end + end + p:close() + else + dt.print(_("unable to get information for ") .. image.filename) + datetime = ERROR + end + return datetime +end + +local function get_image_file_creation_time(image) + -- no exif time in the image file so get the creation time + local datetime = nil + if dt.configuration.running_os == "windows" then + datetime = _get_windows_image_file_creation_time(image) + else + datetime = _get_nix_image_file_creation_time(image) + end + return datetime +end + +local function get_original_image_time(image) + local image_time = image.exif_datetime_taken + local reset_time = nil + + reset_time = get_image_taken_time(image) + + if reset_time then + if reset_time == ERROR then + return image_time + else + return reset_time + end + else + reset_time = get_image_file_creation_time(image) + + if reset_time then + if reset_time == ERROR then + return image_time + else + return reset_time + end + end + end +end + +local function reset_time(images) + if #images > 0 then + for _, image in ipairs(images) do + image.exif_datetime_taken = get_original_image_time(image) + end + else + dt.print_error(_("reset time: no images selected")) + dt.print(_("please select the images that need their time reset")) + end +end + +local function adjust_time(images) + local SEC_MIN = 60 + local SEC_HR = SEC_MIN * 60 + local SEC_DY = SEC_HR * 24 + + local offset = nil + local sign = 1 + + if #images < 1 then + dt.print(_("please select some images and try again")) + return + end + + if img_time.adir.value == _("subtract") then + sign = -1 + end + + for _, image in ipairs(images) do + local y, mo, d, h, m, s = exiftime2vars(image.exif_datetime_taken) + local image_months = year_month2months(y, mo) + local months_diff = year_month2months(img_time.ayr.value, img_time.amo.value) + y, mo = months2year_month(image_months + (months_diff * sign)) + local exif_new = vars2exiftime(y, mo, d, h, m, s) + offset = img_time.ady.value * SEC_DY + offset = offset + img_time.ahr.value * SEC_HR + offset = offset + img_time.amn.value * SEC_MIN + offset = offset + img_time.asc.value + offset = offset * sign + image.exif_datetime_taken = systime2exiftime(exiftime2systime(exif_new) + offset) + end +end + +local function set_time(images) + if #images < 1 then + dt.print(_("please select some images and try again")) + return + end + + local y = img_time.syr.value + local mo = img_time.smo.value + local d = img_time.sdy.value + local h = img_time.shr.value + local m = img_time.smn.value + local s = img_time.ssc.value + + for _, image in ipairs(images) do + image.exif_datetime_taken = vars2exiftime(y, mo, d, h, m, s) + end +end + +local function seq(first, last) + local result = {} + + local num = first + + while num <= last do + table.insert(result, num) + num = num + 1 + end + + return table.unpack(result) +end + +local function reset_widgets() + dt.print_log("took the reset function") + img_time.ayr.selected = 1 + img_time.amo.selected = 1 + img_time.ady.selected = 1 + img_time.ahr.selected = 1 + img_time.ayr.selected = 1 + img_time.amn.selected = 1 + img_time.asc.selected = 1 + img_time.adir.selected = 1 + img_time.syr.selected = #img_time.syr + img_time.smo.selected = 1 + img_time.sdy.selected = 1 + img_time.shr.selected = 1 + img_time.smn.selected = 1 + img_time.ssc.selected = 1 + img_time.adir.selected = 1 +end + +-- widgets + +img_time.widgets = { + -- name, type, tooltip, placeholder, + {"ayr", "combobox", _("years"), _("years to adjust by, 0 - ?"), {seq(0,20)}, 1}, + {"amo", "combobox", _("months"), ("months to adjust by, 0-12"), {seq(0,12)}, 1}, + {"ady", "combobox", _("days"), ("days to adjust by, 0-31"), {seq(0,31)}, 1}, + {"ahr", "combobox", _("hours"), _("hours to adjust by, 0-23"), {seq(0,23)}, 1}, + {"amn", "combobox", _("minutes"), _("minutes to adjust by, 0-59"), {seq(0,59)}, 1}, + {"asc", "combobox", _("seconds"), _("seconds to adjust by, 0-59"), {seq(0,59)}, 1}, + {"adir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1}, + {"syr", "combobox", _("year"), _("year to set, 1900 - now"), {" ", seq(1900,os.date("*t", os.time()).year)}, 1}, + {"smo", "combobox", _("month"), _("month to set, 1-12"), {" ", seq(1,12)}, 1}, + {"sdy", "combobox", _("day"), _("day to set, 1-31"), {" ", seq(1,31)}, 1}, + {"shr", "combobox", _("hour"), _("hour to set, 0-23"), {" ", seq(0,23)}, 1}, + {"smn", "combobox", _("minute"), _("minutes to set, 0-59"), {" ", seq(0, 59)}, 1}, + {"ssc", "combobox", _("seconds"), _("seconds to set, 0-59"), {" ", seq(0,59)}, 1}, + {"sdir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1}, +} + +for _, widget in ipairs(img_time.widgets) do + img_time[widget[1]] = dt.new_widget(widget[2]){ + label = widget[3], + tooltip = widget[4], + selected = widget[6], + table.unpack(widget[5]) + } +end + +img_time.syr.selected = #img_time.syr + +img_time.diff_entry = dt.new_widget("entry"){ + tooltip = _("Time difference between images in seconds"), + placeholder = _("Select 2 images and use the calculate button"), + text = "", +} + +img_time.calc_btn = dt.new_widget("button"){ + label = _("Calculate"), + tooltip = _("calculate time difference between 2 images"), + clicked_callback = function() + calculate_difference(dt.gui.action_images) + end +} + +img_time.btn = dt.new_widget("button"){ + label = _("synchronize image times"), + tooltip = _("apply the time difference from selected images"), + sensitive = false, + clicked_callback = function() + synchronize_time(dt.gui.action_images) + end +} + +img_time.stack = dt.new_widget("stack"){ + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("adjust time")}, + dt.new_widget("section_label"){label = _("days, months, years")}, + img_time.ady, + img_time.amo, + img_time.ayr, + dt.new_widget("section_label"){label = _("hours, minutes, seconds")}, + img_time.ahr, + img_time.amn, + img_time.asc, + dt.new_widget("section_label"){label = _("adjustment direction")}, + img_time.adir, + dt.new_widget("button"){ + label = _("adjust"), + clicked_callback = function() + adjust_time(dt.gui.action_images) + end + } + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("set time")}, + dt.new_widget("section_label"){label = _("date: ")}, + img_time.sdy, + img_time.smo, + img_time.syr, + dt.new_widget("section_label"){label = _("time:")}, + img_time.shr, + img_time.smn, + img_time.ssc, + dt.new_widget("button"){ + label = _("set"), + clicked_callback = function() + set_time(dt.gui.action_images) + end + } + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("synchronize image time")}, + dt.new_widget("section_label"){label = _("calculate difference between images")}, + img_time.diff_entry, + img_time.calc_btn, + dt.new_widget("section_label"){label = _("apply difference")}, + img_time.sdir, + img_time.btn, + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("reset to original time")}, + dt.new_widget("separator"){}, + dt.new_widget("button"){ + label = _("reset"), + clicked_callback = function() + reset_time(dt.gui.action_images) + end + } + }, +} + +img_time.mode = dt.new_widget("combobox"){ + label = _("mode"), + tooltip = _("select mode"), + selected = 1, + changed_callback = function(this) + img_time.stack.active = this.selected + end, + _("adjust time"), + _("set time"), + _("synchronize time"), + _("reset time") +} + +img_time.widget = dt.new_widget("box"){ + orientation = "vertical", + reset_callback = function(this) + reset_widgets() + end, + img_time.mode, + img_time.stack, +} + +dt.register_lib( + "image_time", -- Module name + _("image time"), -- Visible name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers + img_time.widget, + nil,-- view_enter + nil -- view_leave +)