mail2blog.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. --[[ Export image for blog, duplicate and set it LOCKED ]]
  2. local script_path = debug.getinfo(1, "S").source:sub(2):match("(.*/)")
  3. package.path = script_path .. "?.lua;" .. package.path
  4. local config = require("config")
  5. email_1 = config.email_1
  6. email_2 = config.email_2
  7. email_3 = config.email_3
  8. local image_max_x = 2000
  9. local image_max_y = 2000
  10. -- local jpg_quality_str = '92'
  11. -- module name
  12. local MODULE_NAME = "mail2blog"
  13. local df = require "lib/dtutils.file"
  14. local dt = require "darktable"
  15. local du = require "lib/dtutils"
  16. local log = require 'lib/dtutils.log'
  17. local dtsys = require "lib/dtutils.system"
  18. local function quote(text)
  19. return '"' .. text .. '"'
  20. end
  21. local charset = {} do -- [0-9a-zA-Z]
  22. for c = 48, 57 do table.insert(charset, string.char(c)) end
  23. for c = 65, 90 do table.insert(charset, string.char(c)) end
  24. for c = 97, 122 do table.insert(charset, string.char(c)) end
  25. end
  26. -- - - - - - - - - - - - - - - - - - - - - - - -
  27. -- V E R S I O N C H E C K
  28. -- - - - - - - - - - - - - - - - - - - - - - - -
  29. du.check_min_api_version("5.0.2", MODULE_NAME) -- darktable 3.0
  30. -- script_manager integration to allow a script to be removed
  31. -- without restarting darktable
  32. local function destroy()
  33. -- nothing to destroy
  34. end
  35. -- - - - - - - - - - - - - - - - - - - - - - - -
  36. -- C O N S T A N T S
  37. -- - - - - - - - - - - - - - - - - - - - - - - -
  38. local PS = dt.configuration.running_os == "windows" and "\\" or "/"
  39. -- - - - - - - - - - - - - - - - - - - - - - - -
  40. -- T R A N S L A T I O N S
  41. -- - - - - - - - - - - - - - - - - - - - - - - -
  42. local gettext = dt.gettext
  43. gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
  44. local function _(msgid)
  45. return gettext.dgettext(MODULE_NAME, msgid)
  46. end
  47. -- - - - - - - - - - - - - - - - - - - - - - - -
  48. -- M A I N
  49. -- - - - - - - - - - - - - - - - - - - - - - - -
  50. -- alias dt.control.sleep to sleep
  51. local sleep = dt.control.sleep
  52. ---------------------------------------------------------------
  53. -- some helper methods to log information messages
  54. log.log_level(log.info) -- log.info or log.warn or log.debug
  55. local LogCurrentStep = ''
  56. local LogMajorNr = 0
  57. local LogMajorMax = 0
  58. local LogSummaryMessages = {}
  59. local function GetLogInfoText(text)
  60. return '[' .. LogMajorNr .. '/' .. LogMajorMax .. '] ' .. LogCurrentStep .. ': ' .. text
  61. end
  62. local function LogInfo(text)
  63. log.msg(log.info, GetLogInfoText(text))
  64. end
  65. local function LogScreen(text)
  66. log.msg(log.screen, text)
  67. end
  68. local function LogSummaryClear()
  69. for k, v in pairs(LogSummaryMessages) do
  70. LogSummaryMessages[k] = nil
  71. end
  72. end
  73. local function LogSummaryMessage(text)
  74. table.insert(LogSummaryMessages, GetLogInfoText(text))
  75. end
  76. -- ----------------------------------------------------
  77. local function set_published_tag ( email, image )
  78. if email == 'postie_markus_spring.info@markus-spring.de' then
  79. local tagnr = dt.tags.find('photography|published|blog')
  80. dt.tags.attach(tagnr,image)
  81. elseif email == 'postie_vhs_fotogruppe_reichenhall@markus-spring.de' then
  82. local tagnr = dt.tags.find('photography|published|vhs-fotogruppe')
  83. dt.tags.attach(tagnr,image)
  84. -- else
  85. -- local tagnr = dt.tags.find('photography|published|instagram')
  86. -- dt.tags.attach(tagnr,image)
  87. end
  88. local tagnr = dt.tags.find('LOCKED')
  89. dt.tags.attach(tagnr,image)
  90. end
  91. local function getPureFilename(path)
  92. return string.match(path, "([^/\\]+)$")
  93. end
  94. local function set_metadata_note ( tmp_exported, image )
  95. if image then
  96. local current_notes = image.notes or "" -- Retrieve existing notes or set as empty
  97. if current_notes ~= "" then
  98. current_notes = current_notes .. "; "
  99. end
  100. image.notes = current_notes .. 'published as ' .. getPureFilename(tmp_exported)
  101. end
  102. end
  103. local function set_rating_min_2 ( image )
  104. if image.rating < 2 then
  105. image.rating = 2
  106. end
  107. end
  108. local function get_executable( binaryname, binarystring )
  109. local binary = dt.preferences.read(MODULE_NAME, binaryname, "string")
  110. if binary == "" then
  111. dt.print(_( binarystring .. " executable not configured"))
  112. return
  113. end
  114. binary = df.sanitize_filename(binary)
  115. return binary
  116. end
  117. local function run_exiftool ( file, tmp_exported, flags )
  118. local exiftoolbinary = get_executable("exiftoolbinary", "exiftool")
  119. run_cmd = exiftoolbinary..' -TagsFromFile '..file..' '..flags..' '..tmp_exported
  120. LogInfo(string.format("Running %s", run_cmd))
  121. local job = dt.gui.create_job(string.format("Running %s", run_cmd), true, stop_job)
  122. resp = dtsys.external_command(run_cmd)
  123. job.valid = false
  124. end
  125. local function isempty(s)
  126. return s == nil or s == ''
  127. end
  128. ---------------------------------------------------------------
  129. -- helper functions to access darktable feature via user interface
  130. -- use event handling helper functions to wait for pixel pipe
  131. -- processing to complete
  132. local function numberToString(number, nilReplacement, nanReplacement)
  133. -- convert given number to string
  134. -- return 'not a number' and 'nil' as '0/0'
  135. -- log output equals to dt.gui.action command and parameters
  136. if (number ~= number) then
  137. return nanReplacement or '0/0'
  138. end
  139. if (number == nil) then
  140. return nilReplacement or '0/0'
  141. end
  142. -- some digits with dot
  143. local result = string.format('%.4f', number)
  144. result = string.gsub(result, ',', '.')
  145. return result
  146. end
  147. -- convert values to boolean, consider not a number and nil
  148. local function GuiActionValueToBoolean(value)
  149. -- NaN
  150. if (value ~= value) then
  151. return false
  152. end
  153. -- nil
  154. if (value == nil) then
  155. return false
  156. end
  157. return value ~= 0
  158. end
  159. function ThreadSleep(milliseconds)
  160. -- local timeout = StepTimeout:Value()
  161. local timeout = 500
  162. local factor = milliseconds / timeout
  163. LogInfo(' '..string.format(_("wait for %d ms. (config = %s ms * %s)"), milliseconds, timeout, factor))
  164. dt.control.sleep(milliseconds)
  165. end
  166. -- perform the specified effect on the path and element of an action
  167. -- see https://docs.darktable.org/lua/stable/lua.api.manual/darktable/gui/action/
  168. local function GuiActionInternal(path, instance, element, effect, speed, waitForPipeline)
  169. LogInfo('dt.gui.action(' ..
  170. quote(path) ..
  171. ',' .. instance .. ',' .. quote(element) .. ',' .. quote(effect) .. ',' .. numberToString(speed) .. ')')
  172. local result
  173. -- if (waitForPipeline) then
  174. -- WaitForPixelPipe:Do(function()
  175. -- result = dt.gui.action(path, instance, element, effect, speed)
  176. -- end)
  177. -- else
  178. result = dt.gui.action(path, instance, element, effect, speed)
  179. -- wait a bit...
  180. -- ThreadSleep(StepTimeout:Value() / 2)
  181. ThreadSleep(500 / 2)
  182. return result
  183. end
  184. -- wait for 'pixelpipe-processing-complete'
  185. local function GuiAction(path, instance, element, effect, speed)
  186. return GuiActionInternal(path, instance, element, effect, speed, true)
  187. end
  188. local function randomString(length)
  189. if not length or length <= 0 then return '' end
  190. math.randomseed(os.clock()//5)
  191. return randomString(length - 1) .. charset[math.random(1, #charset)]
  192. end
  193. -- Function to replace German umlauts and sanitize the filename
  194. local function create_safe_filename(title)
  195. local replacements = {
  196. ["ä"] = "ae", ["ö"] = "oe", ["ü"] = "ue",
  197. ["Ä"] = "Ae", ["Ö"] = "Oe", ["Ü"] = "Ue",
  198. ["ß"] = "ss"
  199. }
  200. title = title:gsub("[%z\1-\127\194-\244][\128-\191]*", function(char)
  201. return replacements[char] or char
  202. end)
  203. title = title:gsub("[^%w%-]", "_")
  204. title = title:gsub("_+", "_")
  205. title = title:gsub("^_+", ""):gsub("_+$", "")
  206. if title == "" then
  207. title = "untitled"
  208. end
  209. title = title:sub(1, 255)
  210. return title
  211. end
  212. -- ----------------------------------------------------
  213. dt.print_log(MODULE_NAME .. ' loaded')
  214. -- save the configuration
  215. local current_view = dt.gui.current_view()
  216. local select_publication_target = dt.new_widget("combobox")
  217. {
  218. label = "target",
  219. tooltip = "Select blog for image publication",
  220. changed_callback = function(w) dt.preferences.write(MODULE_NAME, "target", "string", w.selected) end,
  221. email_1, email_2, email_3
  222. }
  223. local publication_button = dt.new_widget("button")
  224. {
  225. label = "Publish!",
  226. clicked_callback = function ()
  227. for i_, i in ipairs(dt.gui.action_images) do
  228. if isempty(i.title) then
  229. local job = dt.gui.create_job("FEHLER: Das Bild " .. i.filename .. " hat keinen Titel.", true, stop_job)
  230. os.execute("sleep " .. 3)
  231. job.valid = false
  232. else
  233. set_rating_min_2( i )
  234. -- create duplicate
  235. local newimg = i.duplicate_with_history(i)
  236. local jpeg_exporter = dt.new_format("jpeg")
  237. jpeg_exporter.max_height = image_max_y
  238. jpeg_exporter.max_width = image_max_x
  239. -- local tmp_exported = os.tmpname()..".jpg"
  240. local tempname = os.tmpname()
  241. os.remove(tempname)
  242. os.execute("mkdir " .. tempname)
  243. local tmp_exported = tempname .. '/' .. create_safe_filename(i.title) .. '.jpg'
  244. local job = dt.gui.create_job(string.format(_("Converting raw file '%s' to jpeg..."), i.filename), true, stop_job)
  245. jpeg_exporter:write_image(i, tmp_exported, false)
  246. -- copy exif data from original file
  247. run_exiftool( df.sanitize_filename(i.path..PS..i.filename), tmp_exported, '-exif:all --subifd:all --Orientation -overwrite_original' )
  248. -- copy exif data from xmp file
  249. run_exiftool( df.sanitize_filename(i.sidecar) , tmp_exported, '-xmp:all -exif:all --subifd:all -overwrite_original' )
  250. -- run mailscript
  251. local spring2lifescript = get_executable("spring2lifescript", "spring2life script")
  252. run_cmd = spring2lifescript .. " " .. tmp_exported
  253. .. ' ' .. select_publication_target.value
  254. .. ' ' .. df.sanitize_filename(i.filename) .. ' &'
  255. LogInfo(run_cmd)
  256. job = false
  257. local job = dt.gui.create_job("Running " .. run_cmd, true, stop_job)
  258. os.execute(run_cmd)
  259. set_published_tag ( select_publication_target.value, i )
  260. job = false
  261. -- set_metadata_note ( tmp_exported, i )
  262. LogScreen("Done")
  263. --- dt.styles.delete(tmpstyle)
  264. end
  265. end
  266. end
  267. }
  268. local lib_widgets = {}
  269. table.insert(lib_widgets, select_publication_target)
  270. table.insert(lib_widgets, publication_button)
  271. -- ... and tell dt about it all
  272. dt.register_lib(
  273. MODULE_NAME, -- plugin name
  274. MODULE_NAME, -- name
  275. true, -- expandable
  276. false, -- resetable
  277. {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
  278. dt.new_widget("box") -- widget
  279. {
  280. orientation = "vertical",
  281. -- sensitive = enfuse_installed,
  282. table.unpack(lib_widgets)
  283. },
  284. nil,-- view_enter
  285. nil -- view_leave
  286. )
  287. -- end
  288. -- -- register the new preferences -----------------------------------------------
  289. dt.preferences.register(MODULE_NAME, "exiftoolbinary", "file",
  290. _(MODULE_NAME .. ": executable for exiftool"),
  291. _("select executable for exiftool command line version") , "")
  292. dt.preferences.register(MODULE_NAME, "spring2lifescript", "file",
  293. _(MODULE_NAME .. ": executable for mail2spring2life script"),
  294. _("select executable for mail2spring2life script") , "")
  295. -- ----------------------------------------------------------------------------------
  296. -- set the destroy routine so that script_manager can call it when
  297. -- it's time to destroy the script and then return the data to
  298. -- script_manager
  299. local script_data = {}
  300. script_data.destroy = destroy
  301. return script_data