From 3675f4d7cdfba921653c982fe4924a8310730120 Mon Sep 17 00:00:00 2001 From: theres Date: Sun, 27 Jan 2019 02:34:01 +0100 Subject: [PATCH] video ffmpeg - post review fixes - rename OS_PATH_SEPARATOR -> PS and use in all needed places - change function name from `open_file` to `launch_default_app` - move `mkdir`, `rm` and `launch_default_app` helper functions to libs - add executable_path_widget to export module if needed, not to the preferences - add show_status - remove support_format - change function name `format` to `format_string`, add comments - replace apostrophes by quote marks --- contrib/timelapse.lua | 463 --------------------------------------- contrib/video_ffmpeg.lua | 448 +++++++++++++++++++++++++++++++++++++ lib/dtutils/file.lua | 46 ++++ lib/dtutils/system.lua | 30 +++ 4 files changed, 524 insertions(+), 463 deletions(-) delete mode 100644 contrib/timelapse.lua create mode 100644 contrib/video_ffmpeg.lua diff --git a/contrib/timelapse.lua b/contrib/timelapse.lua deleted file mode 100644 index 2976cc7..0000000 --- a/contrib/timelapse.lua +++ /dev/null @@ -1,463 +0,0 @@ ---[[Timelapse video plugin for darktable 2.4.X and 2.6.X - - copyright (c) 2018, 2019 Dominik Markiewicz - - darktable 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. - - darktable 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 darktable. If not, see . -]] - ---[[About this plugin -This plugin will add the new export module "timelapse". - -----REQUIRED SOFTWARE---- -ffmpeg - -----USAGE---- -1. Go to Lighttable -2. Select images you want to use as a timelapse frames -3. In image export module select 'timelapse' -4. Configure you video settings -5. Export - -----WARNING---- -This script has been tested under Linux only -]] - -local dt = require "darktable" -local df = require "lib/dtutils.file" -local dsys = require "lib/dtutils.system" -local gettext = dt.gettext - -dt.configuration.check_version(...,{5,0,0}) - -local MODULE_NAME = 'timelapse' -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..'/lua/') - -local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) -end - -local OS_PATH_SEPARATOR = dt.configuration.running_os == "windows" and "\\" or "/" - ----- DECLARATIONS - -local resolutions = { - ['QVGA'] = { - ['label'] = 'QVGA 320x240 (4:3)', - ['w'] = 320, - ['h'] = 240 - }, - ['HVGA'] = { - ['label'] = 'HVGA 480x320 (3:2)', - ['w'] = 480, - ['h'] = 320 - }, - ['VGA'] = { - ['label'] = 'VGA 640x480 (4:3)', - ['w'] = 640, - ['h'] = 480 - }, - ['HDTV 720p'] = { - ['label'] = 'HDTV 720p 1280x720 (16:9)', - ['w'] = 1280, - ['h'] = 720 - }, - ['HDTV 1080p'] = { - ['label'] = 'HDTV 1080p 1920x1080 (16:9)', - ['w'] = 1920, - ['h'] = 1080 - }, - ['Cinema TV'] = { - ['label'] = 'Cinema TV 2560x1080 (21:9)', - ['w'] = 2560, - ['h'] = 1080 - }, - ['2K'] = { - ['label'] = '2K 2048x1152 (16:9)', - ['w'] = 2048, - ['h'] = 1152 - }, - ['4K'] = { - ['label'] = '4K 4096x2304 (16:9)', - ['w'] = 4096, - ['h'] = 2304 - } -} - -local framerates = {'15', '16', '23.98', '24', '25', '29,97', '30', '48', '50', '59.94', '60'} - -local formats = { - ['AVI'] = { - ['extension'] = 'avi', - ['codecs'] = {'mpeg4', 'h263p', 'h264', 'mpeg2video', 'hevc', 'vp9'} - }, - ['Matroska'] = { - ['extension'] = 'mkv', - ['codecs'] = {'h264', 'h263p', 'mpeg4', 'mpeg2video', 'hevc', 'vp9'} - }, - ['WebM'] = { - ['extension'] = 'webm', - ['codecs'] = {'vp9', 'h263p', 'h264', 'mpeg4', 'mpeg2video', 'hevc'} - }, - ['MP4'] = { - ['extension'] = 'mp4', - ['codecs'] = {'h264', 'h263p', 'mpeg4', 'mpeg2video'} - }, - ['QuickTime'] = { - ['extension'] = 'mov', - ['codecs'] = {'h264', 'h263p', 'mpeg4', 'mpeg2video'} - } -} - -res_list = {} -for i, v in pairs(resolutions) do - table.insert(res_list, v['label']) -end - -table.sort(res_list) - -format_list = {} -for i,v in pairs(formats) do - table.insert(format_list, i) -end - -table.sort(format_list) - -codec_list = formats['AVI']['codecs'] -table.sort(codec_list) - -local function extract_resolution(description) - for _, v in pairs(resolutions) do - if v['label'] == description then - return v['w']..'x'..v['h'] - end - end - return '100x100' -end - ----- ENSURE ffmpeg is INSTALLED - -dt.print_log(MODULE_NAME .. " - Executable Path Preference: "..df.get_executable_path_preference("ffmpeg")) -local ffmpeg_path = df.check_if_bin_exists("ffmpeg") -if not ffmpeg_path then - dt.print_error("ffmpeg not found") - dt.print("ERROR - ffmpeg not found") -end - -local ffmpeg_path_widget = df.executable_path_widget({'ffmpeg'}) -dt.preferences.register("executable_paths", "ffmpeg", - "file", - MODULE_NAME .. ': ffmpeg location', - 'Install location of ffmpeg', - "ffmpeg", - ffmpeg_path_widget -) - ----- GENERIC UTILS - -local function replace_combobox_elements(combobox, new_items, to_select) - if to_select == nil then - to_select = combobox.value - end - - to_select_idx = 1 - for i , name in ipairs(new_items) do - if name == to_select then - to_select_idx = i - break - end - end - - local old_elements_count = #combobox - for i, name in ipairs(new_items) do - combobox[i] = name - end - if old_elements_count > #new_items then - for j = old_elements_count, #new_items + 1, -1 do - combobox[j] = nil - end - end - - combobox.value = to_select_idx -end - -local function format(label, symbols) - local es1, es2 = "\u{ffe0}", "\u{ffe1}" -- for simplicity, just some strange utf characters - local result = label:gsub("\\{", es1):gsub("\\}", es2) - for s,v in pairs(symbols) do - result = result:gsub("{"..s.."}", v) - end - return result:gsub(es1, "{"):gsub(es2, "}") -end - -local function open_file(filename) - local open_cmd = 'xdg-open' - if (dt.configuration.running_os == 'windows') then - open_cmd = 'start' - elseif (dt.configuration.running_os == 'macos') then - open_cmd = 'open' - end - return dsys.external_command(open_cmd..' '..df.sanitize_filename(filename)) -end - -local function mkdir(path) - local mkdir_cmd = dt.configuration.running_os == 'windows' and 'mkdir' or 'mkdir -p' - return dsys.external_command(mkdir_cmd..' '..df.sanitize_filename(path)) -end - -local function rm(path) - local rm_cmd = dt.configuration.running_os == 'windows' and 'rmdir /S /Q' or 'rm -r' - return dsys.external_command(rm_cmd..' '..df.sanitize_filename(path)) -end - ------ COMPONENTS - -local function combobox_pref_read(name, all_values) - local value = dt.preferences.read(MODULE_NAME, name, "string") - for i,v in pairs(all_values) do - if v == value then return i end - end - return 1 -end - -local function combobox_pref_write(name) - local writer = function(widget) - dt.preferences.write(MODULE_NAME, name, "string", widget.value) - end - return writer -end - -local function string_pref_read(name, default) - local value = dt.preferences.read(MODULE_NAME, name, "string") - if value ~= nil and value ~= "" then return value end - return default -end - -local function string_pref_write(name, widget_attribute) - widget_attribute = widget_attribute or "value" - local writer = function(widget) - dt.preferences.write(MODULE_NAME, name, "string", widget[widget_attribute]) - end - return writer -end - -local framerates_selector = dt.new_widget('combobox'){ - label = _('framerate'), - tooltip = _('select framerate of output video'), - value = combobox_pref_read("framerate", framerates), - changed_callback = combobox_pref_write('framerate'), - table.unpack(framerates) -} - -local res_selector = dt.new_widget('combobox'){ - label = _('resolution'), - tooltip = _('select resolution of output video'), - value = combobox_pref_read('resolution', res_list), - changed_callback = combobox_pref_write('resolution'), - table.unpack(res_list) -} - -local codec_selector = dt.new_widget('combobox'){ - label = _('codec'), - tooltip = _('select codec'), - value = combobox_pref_read('codec', codec_list), - changed_callback = combobox_pref_write('codec'), - table.unpack(codec_list) -} - -local format_selector = dt.new_widget('combobox'){ - label = _('format container'), - tooltip = _('select format of output video'), - value = combobox_pref_read('format', format_list), - changed_callback = function(widget) - combobox_pref_write('format')(widget) - codec_list = formats[widget.value]['codecs'] - table.sort(codec_list) - replace_combobox_elements(codec_selector, codec_list) - end, - table.unpack(format_list) -} - -local destination_label = dt.new_widget('section_label'){ - label = _('output file destination'), - tooltip = _('settings of output file destination and name') -} - -local output_directory_chooser = dt.new_widget('file_chooser_button'){ - title = _('Select export path'), - is_directory = true, - tooltip =_('select the target directory for the timelapse. \nthe filename is created automatically.'), - value = string_pref_read("export_path", os.getenv('HOME')), - changed_callback = string_pref_write("export_path") -} - -local auto_output_directory_btn = dt.new_widget('check_button') { - label = '', - tooltip = _('if selected, output video will be placed in the same directory as first of selected images'), - value = not dt.preferences.read(MODULE_NAME, "not_auto_output_directory", "bool"), -- reverse, for true as default - clicked_callback = function (widget) - dt.preferences.write(MODULE_NAME, "not_auto_output_directory", "bool", not widget.value) - output_directory_chooser.sensitive = not output_directory_chooser.sensitive - end -} - -local destination_box = dt.new_widget('box') { - orientation = 'horizontal', - auto_output_directory_btn, - output_directory_chooser -} - -local override_output_cb = dt.new_widget('check_button'){ - label = _('override output file on conflict'), - tooltip = _('if checked, in case of file name conflict, the file will be overwritten'), - value = dt.preferences.read(MODULE_NAME, "override_output", "bool"), - clicked_callback = function (widget) - dt.preferences.write(MODULE_NAME, "override_output", "bool", widget.value) - end -} - -local filename_entry = dt.new_widget('entry'){ - tooltip = _("enter output file name without extension.\n\n".. - "You can use some placeholders:\n".. - "- {time} - time in format HH-mm-ss\n".. - "- {date} - date in foramt YYYY-mm-dd\n".. - "- {first_file} - name of first input file\n".. - "- {last_file} - name of last last_file" - ), - text = string_pref_read("filename_entry","timelapse_{date}_{time}"), - -- changed_callback = string_pref_write("filename_entry", "text") -- Not imlemented yet -} - - -local output_box = dt.new_widget('box'){ - orientation='vertical', - destination_label, - override_output_cb, - destination_box, - filename_entry, -} - -local open_after_export_cb = dt.new_widget('check_button'){ - label = _(' open after export'), - tooltip = _('open video file after successful export'), - value = dt.preferences.read(MODULE_NAME, "open_after_export", "bool"), - clicked_callback = function (widget) - dt.preferences.write(MODULE_NAME, "open_after_export", "bool", widget.value) - end -} - -local module_widget = dt.new_widget('box') { - orientation = 'vertical', - res_selector, - framerates_selector, - format_selector, - codec_selector, - output_box, - open_after_export_cb -} - ----- EXPORT & REGISTRATION - -local function support_format(storage, format) - return true -end - -local function init_export(storage, img_format, images, high_quality, extra_data) - -- store filename preference here cause there is no changed_callback on entry yet - string_pref_write("filename_entry", "text")(filename_entry) - - extra_data['images'] = images -- needed, to preserve images order - extra_data['tmp_dir'] = dt.configuration.tmp_dir .. '/'..MODULE_NAME .. '_' .. os.time() - extra_data['fps'] = framerates_selector.value - extra_data['res'] = extract_resolution(res_selector.value) - extra_data['codec'] = codec_selector.value - extra_data['img_ext'] = '.'..img_format.extension - local override_output = override_output_cb.value - local output_directory = auto_output_directory_btn.value and images[1].path or output_directory_chooser.value - local filename_mappings = { - date = os.date("%Y-%m-%d"), - time = os.date("%H-%M-%S"), - first_file = images[1].filename, - last_file = images[#images].filename - } - local output_extension = '.'..formats[format_selector.value]['extension'] - local filename = format(filename_entry.text, filename_mappings) - local path = output_directory..OS_PATH_SEPARATOR..filename..output_extension - if not override_output then - path = df.create_unique_filename(path) - end - - extra_data['output_file'] = path - extra_data['open_after_export'] = open_after_export_cb.value -end - -local function export(extra_data) - local ffmpeg_path = df.check_if_bin_exists("ffmpeg") - if not ffmpeg_path then - dt.print_error("ffmpeg not found") - dt.print("ERROR - ffmpeg not found") - return - end - local dir = extra_data['tmp_dir'] - local fps = extra_data['fps'] - local res = extra_data['res'] - local codec = extra_data['codec'] - local img_ext = extra_data['img_ext'] - local output_file = extra_data['output_file'] - - local dir_create_result = mkdir(df.get_path(output_file)) - if dir_create_result ~= 0 then return dir_create_result end - - local cmd = ffmpeg_path..' -y -r '..fps..' -i '..dir..OS_PATH_SEPARATOR..'%d'..img_ext..' -s:v '..res..' -c:v '..codec..' -crf 18 -preset veryslow '..df.sanitize_filename(output_file) - return dsys.external_command(cmd), output_file -end - -local function finalize_export(storage, images_table, extra_data) - local tmp_dir = extra_data['tmp_dir'] - - dt.print(_('prepare merge process')) - - local result = mkdir(tmp_dir) - if result ~= 0 then dt.print(_('ERROR: cannot create temp directory')) end - - local images = extra_data['images'] - -- rename all images to consecutive numbers - for i, file in pairs(images) do - local filename = images_table[file] - dt.print_error(filename, file.filename) - df.file_move(filename, tmp_dir .. OS_PATH_SEPARATOR .. i .. extra_data['img_ext']) - end - dt.print('Start video building...') - local result, path = export(extra_data) - if result ~= 0 then - dt.print(_('ERROR: cannot build image, see console for more info')) - else - dt.print(_('SUCCESS')) - if extra_data['open_after_export'] then - open_file(path) - end - end - - rm(tmp_dir) -end - -dt.register_storage( - 'module_timelapse', - _(MODULE_NAME), - nil, - finalize_export, - support_format, - init_export, - module_widget -) - diff --git a/contrib/video_ffmpeg.lua b/contrib/video_ffmpeg.lua new file mode 100644 index 0000000..9b8c188 --- /dev/null +++ b/contrib/video_ffmpeg.lua @@ -0,0 +1,448 @@ +--[[Timelapse video plugin based on ffmpeg for darktable 2.4.X and 2.6.X + + copyright (c) 2018, 2019 Dominik Markiewicz + + darktable 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. + + darktable 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 darktable. If not, see . +]] + +--[[About this plugin +This plugin will add the new export module "video ffmpeg". + +----REQUIRED SOFTWARE---- +ffmpeg + +----USAGE---- +1. Go to Lighttable +2. Select images you want to use as a video ffmpeg frames +3. In image export module select "video ffmpeg" +4. Configure you video settings +5. Export + +----WARNING---- +This script has been tested under Linux only +]] + +local dt = require "darktable" +local df = require "lib/dtutils.file" +local dsys = require "lib/dtutils.system" +local gettext = dt.gettext + +dt.configuration.check_version(...,{5,0,0}) + +local MODULE_NAME = "video_ffmpeg" + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) + +local function _(msgid) + return gettext.dgettext(MODULE_NAME, msgid) +end + + +---- DECLARATIONS + +local resolutions = { + ["QVGA"] = { + ["label"] = "QVGA 320x240 (4:3)", + ["w"] = 320, + ["h"] = 240 + }, + ["HVGA"] = { + ["label"] = "HVGA 480x320 (3:2)", + ["w"] = 480, + ["h"] = 320 + }, + ["VGA"] = { + ["label"] = "VGA 640x480 (4:3)", + ["w"] = 640, + ["h"] = 480 + }, + ["HDTV 720p"] = { + ["label"] = "HDTV 720p 1280x720 (16:9)", + ["w"] = 1280, + ["h"] = 720 + }, + ["HDTV 1080p"] = { + ["label"] = "HDTV 1080p 1920x1080 (16:9)", + ["w"] = 1920, + ["h"] = 1080 + }, + ["Cinema TV"] = { + ["label"] = "Cinema TV 2560x1080 (21:9)", + ["w"] = 2560, + ["h"] = 1080 + }, + ["2K"] = { + ["label"] = "2K 2048x1152 (16:9)", + ["w"] = 2048, + ["h"] = 1152 + }, + ["4K"] = { + ["label"] = "4K 4096x2304 (16:9)", + ["w"] = 4096, + ["h"] = 2304 + } +} + +local framerates = {"15", "16", "23.98", "24", "25", "29,97", "30", "48", "50", "59.94", "60"} + +local formats = { + ["AVI"] = { + ["extension"] = "avi", + ["codecs"] = {"mpeg4", "h263p", "h264", "mpeg2video", "hevc", "vp9"} + }, + ["Matroska"] = { + ["extension"] = "mkv", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video", "hevc", "vp9"} + }, + ["WebM"] = { + ["extension"] = "webm", + ["codecs"] = {"vp9", "h263p", "h264", "mpeg4", "mpeg2video", "hevc"} + }, + ["MP4"] = { + ["extension"] = "mp4", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video"} + }, + ["QuickTime"] = { + ["extension"] = "mov", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video"} + } +} + +local res_list = {} +for i, v in pairs(resolutions) do + table.insert(res_list, v["label"]) +end + +table.sort(res_list) + +-- inital fulfill list of all available formats +local format_list = {} +for i,v in pairs(formats) do + table.insert(format_list, i) +end +table.sort(format_list) + +-- initial fulfill list of all available codecs for the first (default if nothig saved yet) format +-- if format was changed and stored in preferences, this list will be replaced by one matching selected format +local codec_list = formats[format_list[1]]["codecs"] +table.sort(codec_list) + +local function extract_resolution(description) + for _, v in pairs(resolutions) do + if v["label"] == description then + return v["w"].."x"..v["h"] + end + end + return "100x100" +end + +---- GENERIC UTILS + +local function replace_combobox_elements(combobox, new_items, to_select) + if to_select == nil then + to_select = combobox.value + end + + to_select_idx = 1 + for i , name in ipairs(new_items) do + if name == to_select then + to_select_idx = i + break + end + end + + local old_elements_count = #combobox + for i, name in ipairs(new_items) do + combobox[i] = name + end + if old_elements_count > #new_items then + for j = old_elements_count, #new_items + 1, -1 do + combobox[j] = nil + end + end + + combobox.value = to_select_idx +end + +--[[ +This function allow to format string by substitute tokens like `{label}` by value provided in `symbols` table + ex: +```lua + format_string("I like {bananas} and {potatos}. {bananas}!", {["bananas"]: "darktable", ["potatos"]: "lua"}) + -> "I like darktable and lua. darktable!" +``` + +I you want to preserve given currly braces, you need to escape it by backslash like so: +```lua + format_string("it preserve \\{label\\} but substitute {label}", {["label"]: "this"}) + -> "It preserve {label} but substitute " +``` +--]] +local function format_string(label, symbols) + local es1, es2 = "\u{ffe0}", "\u{ffe1}" -- for simplicity, just some strange utf characters + local result = label:gsub("\\{", es1):gsub("\\}", es2) + for s,v in pairs(symbols) do + result = result:gsub("{"..s.."}", v) + end + return result:gsub(es1, "{"):gsub(es2, "}") +end + +----- COMPONENTS + +local function combobox_pref_read(name, all_values) + local value = dt.preferences.read(MODULE_NAME, name, "string") + for i,v in pairs(all_values) do + if v == value then return i end + end + return 1 +end + +local function combobox_pref_write(name) + local writer = function(widget) + dt.preferences.write(MODULE_NAME, name, "string", widget.value) + end + return writer +end + +local function string_pref_read(name, default) + local value = dt.preferences.read(MODULE_NAME, name, "string") + if value ~= nil and value ~= "" then return value end + return default +end + +local function string_pref_write(name, widget_attribute) + widget_attribute = widget_attribute or "value" + local writer = function(widget) + dt.preferences.write(MODULE_NAME, name, "string", widget[widget_attribute]) + end + return writer +end + +local framerates_selector = dt.new_widget("combobox"){ + label = _("framerate"), + tooltip = _("select framerate of output video"), + value = combobox_pref_read("framerate", framerates), + changed_callback = combobox_pref_write("framerate"), + table.unpack(framerates) +} + +local res_selector = dt.new_widget("combobox"){ + label = _("resolution"), + tooltip = _("select resolution of output video"), + value = combobox_pref_read("resolution", res_list), + changed_callback = combobox_pref_write("resolution"), + table.unpack(res_list) +} + +local codec_selector = dt.new_widget("combobox"){ + label = _("codec"), + tooltip = _("select codec"), + value = combobox_pref_read("codec", codec_list), + changed_callback = combobox_pref_write("codec"), + table.unpack(codec_list) +} + +local format_selector = dt.new_widget("combobox"){ + label = _("format container"), + tooltip = _("select format of output video"), + value = combobox_pref_read("format", format_list), + changed_callback = function(widget) + combobox_pref_write("format")(widget) + codec_list = formats[widget.value]["codecs"] + table.sort(codec_list) + replace_combobox_elements(codec_selector, codec_list) + end, + table.unpack(format_list) +} + +local destination_label = dt.new_widget("section_label"){ + label = _("output file destination"), + tooltip = _("settings of output file destination and name") +} + +local output_directory_chooser = dt.new_widget("file_chooser_button"){ + title = _("Select export path"), + is_directory = true, + tooltip =_("select the target directory for the timelapse. \nthe filename is created automatically."), + value = string_pref_read("export_path", os.getenv("HOME")), + changed_callback = string_pref_write("export_path") +} + +local auto_output_directory_btn = dt.new_widget("check_button") { + label = "", + tooltip = _("if selected, output video will be placed in the same directory as first of selected images"), + value = not dt.preferences.read(MODULE_NAME, "not_auto_output_directory", "bool"), -- reverse, for true as default + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "not_auto_output_directory", "bool", not widget.value) + output_directory_chooser.sensitive = not output_directory_chooser.sensitive + end +} + +local destination_box = dt.new_widget("box") { + orientation = "horizontal", + auto_output_directory_btn, + output_directory_chooser +} + +local override_output_cb = dt.new_widget("check_button"){ + label = _("override output file on conflict"), + tooltip = _("if checked, in case of file name conflict, the file will be overwritten"), + value = dt.preferences.read(MODULE_NAME, "override_output", "bool"), + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "override_output", "bool", widget.value) + end +} + +local filename_entry = dt.new_widget("entry"){ + tooltip = _("enter output file name without extension.\n\n".. + "You can use some placeholders:\n".. + "- {time} - time in format HH-mm-ss\n".. + "- {date} - date in foramt YYYY-mm-dd\n".. + "- {first_file} - name of first input file\n".. + "- {last_file} - name of last last_file" + ), + text = string_pref_read("filename_entry","timelapse_{date}_{time}"), + -- changed_callback = string_pref_write("filename_entry", "text") -- Not imlemented yet +} + +local output_box = dt.new_widget("box"){ + orientation="vertical", + destination_label, + override_output_cb, + destination_box, + filename_entry, +} + +local open_after_export_cb = dt.new_widget("check_button"){ + label = _(" open after export"), + tooltip = _("open video file after successful export"), + value = dt.preferences.read(MODULE_NAME, "open_after_export", "bool"), + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "open_after_export", "bool", widget.value) + end +} + +local widgets_list = {} +if not df.check_if_bin_exists("ffmpeg") then + table.insert(timelapse_widgets, df.executable_path_widget("ffmpeg")) +end +table.insert(widgets_list, res_selector) +table.insert(widgets_list, framerates_selector) +table.insert(widgets_list, format_selector) +table.insert(widgets_list, codec_selector) +table.insert(widgets_list, output_box) +table.insert(widgets_list, open_after_export_cb) + + +local module_widget = dt.new_widget("box") { + orientation = "vertical", + table.unpack(widgets_list) +} + +---- EXPORT & REGISTRATION + +local function show_status(enf_storage, image, format, filename, number, total, high_quality, extra_data) + dt.print(_("export ")..tostring(number).." / "..tostring(total)) +end + +local function init_export(storage, img_format, images, high_quality, extra_data) + -- store filename preference here cause there is no changed_callback on entry yet + string_pref_write("filename_entry", "text")(filename_entry) + + extra_data["images"] = images -- needed, to preserve images order + extra_data["tmp_dir"] = dt.configuration.tmp_dir .. "/"..MODULE_NAME .. "_" .. os.time() + extra_data["fps"] = framerates_selector.value + extra_data["res"] = extract_resolution(res_selector.value) + extra_data["codec"] = codec_selector.value + extra_data["img_ext"] = "."..img_format.extension + local override_output = override_output_cb.value + local output_directory = auto_output_directory_btn.value and images[1].path or output_directory_chooser.value + local filename_mappings = { + date = os.date("%Y-%m-%d"), + time = os.date("%H-%M-%S"), + first_file = images[1].filename, + last_file = images[#images].filename + } + local output_extension = "."..formats[format_selector.value]["extension"] + local filename = format_string(filename_entry.text, filename_mappings) + local path = output_directory..PS..filename..output_extension + if not override_output then + path = df.create_unique_filename(path) + end + + extra_data["output_file"] = path + extra_data["open_after_export"] = open_after_export_cb.value +end + +local function export(extra_data) + local ffmpeg_path = df.check_if_bin_exists("ffmpeg") + if not ffmpeg_path then + dt.print_error("ffmpeg not found") + dt.print("ERROR - ffmpeg not found") + return + end + local dir = extra_data["tmp_dir"] + local fps = extra_data["fps"] + local res = extra_data["res"] + local codec = extra_data["codec"] + local img_ext = extra_data["img_ext"] + local output_file = extra_data["output_file"] + + local dir_create_result = df.mkdir(df.get_path(output_file)) + if dir_create_result ~= 0 then return dir_create_result end + + local cmd = ffmpeg_path.." -y -r "..fps.." -i "..dir..PS.."%d"..img_ext.." -s:v "..res.." -c:v "..codec.." -crf 18 -preset veryslow "..df.sanitize_filename(output_file) + return dsys.external_command(cmd), output_file +end + +local function finalize_export(storage, images_table, extra_data) + local tmp_dir = extra_data["tmp_dir"] + + dt.print(_("prepare merge process")) + + local result = df.mkdir(tmp_dir) + if result ~= 0 then dt.print(_("ERROR: cannot create temp directory")) end + + local images = extra_data["images"] + -- rename all images to consecutive numbers + for i, file in pairs(images) do + local filename = images_table[file] + dt.print_error(filename, file.filename) + df.file_move(filename, tmp_dir .. PS .. i .. extra_data["img_ext"]) + end + dt.print("Start video building...") + local result, path = export(extra_data) + if result ~= 0 then + dt.print(_("ERROR: cannot build image, see console for more info")) + else + dt.print(_("SUCCESS")) + if extra_data["open_after_export"] then + dsys.launch_default_app(path) + end + end + + df.rm(tmp_dir) +end + +dt.register_storage( + "module_video_ffmpeg", + _(MODULE_NAME), + show_status, + finalize_export, + nil, + init_export, + module_widget +) + diff --git a/lib/dtutils/file.lua b/lib/dtutils/file.lua index 120b70f..4015e3a 100644 --- a/lib/dtutils/file.lua +++ b/lib/dtutils/file.lua @@ -1,6 +1,7 @@ local dtutils_file = {} local dt = require "darktable" local ds = require "lib/dtutils.string" +local dsys = require "lib/dtutils.system" local log = require "lib/dtutils.log" @@ -553,5 +554,50 @@ function dtutils_file.sanitize_filename(filename) return ds.sanitize(filename) end +dtutils_file.libdoc.functions["mkdir"] = { + Name = [[mkdir]], + Synopsis = [[create the directory(ies) if they do not already exists]], + Usage = [[local df = require "lib/dtutils.file" + + df.mkdir(path) + path - string - a directory path]], + Description = [[mkdir creates directories if not already exists. It + create whole parents subtree if needed + ]], + Return_Value = [[path - string - a directory path]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} +function dtutils_file.mkdir(path) + local mkdir_cmd = dt.configuration.running_os == "windows" and "mkdir" or "mkdir -p" + return dsys.external_command(mkdir_cmd.." "..dtutils_file.sanitize_filename(path)) +end + +dtutils_file.libdoc.functions["rm"] = { + Name = [[rm]], + Synopsis = [[remove file or directory]], + Usage = [[local df = require "lib/dtutils.file" + + df.rm(path) + path - string - a file or directory path]], + Description = [[rm allow to recursively remove files or directories]], + Return_Value = [[path - string - a file or directory path]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} +function dtutils_file.rm(path) + local rm_cmd = dt.configuration.running_os == "windows" and "rmdir /S /Q" or "rm -r" + return dsys.external_command(rm_cmd.." "..dtutils_file.sanitize_filename(path)) +end + + return dtutils_file diff --git a/lib/dtutils/system.lua b/lib/dtutils/system.lua index 0425a10..c40fc0c 100644 --- a/lib/dtutils/system.lua +++ b/lib/dtutils/system.lua @@ -101,4 +101,34 @@ function dtutils_system.windows_command(command) return result end + +dtutils_system.libdoc.functions["launch_default_app"] = { + Name = [[launch_default_app]], + Synopsis = [[try to open file in default application]], + Usage = [[local dsys = require "lib/dtutils.file" + + dsys.launch_default_app(path) + path - string - a file path]], + Description = [[launch_default_app allow to open file in application that is assigned as default + one in users's system for given filetype + ]], + Return_Value = [[path - string - a file path]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} +function dtutils_system.launch_default_app(path) + local open_cmd = "xdg-open" + if (dt.configuration.running_os == "windows") then + open_cmd = "start" + elseif (dt.configuration.running_os == "macos") then + open_cmd = "open" + end + return dtutils_system.external_command(open_cmd..' "'..path..'"') +end + + return dtutils_system \ No newline at end of file