mail2blog.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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 = 2500
  9. local image_max_y = 2500
  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 cjson = require "cjson"
  19. local function quote(text)
  20. return '"' .. text .. '"'
  21. end
  22. local charset = {} do -- [0-9a-zA-Z]
  23. for c = 48, 57 do table.insert(charset, string.char(c)) end
  24. for c = 65, 90 do table.insert(charset, string.char(c)) end
  25. for c = 97, 122 do table.insert(charset, string.char(c)) end
  26. end
  27. -- - - - - - - - - - - - - - - - - - - - - - - -
  28. -- V E R S I O N C H E C K
  29. -- - - - - - - - - - - - - - - - - - - - - - - -
  30. du.check_min_api_version("5.0.2", MODULE_NAME) -- darktable 3.0
  31. -- script_manager integration to allow a script to be removed
  32. -- without restarting darktable
  33. local function destroy()
  34. -- nothing to destroy
  35. end
  36. -- - - - - - - - - - - - - - - - - - - - - - - -
  37. -- C O N S T A N T S
  38. -- - - - - - - - - - - - - - - - - - - - - - - -
  39. local PS = dt.configuration.running_os == "windows" and "\\" or "/"
  40. -- - - - - - - - - - - - - - - - - - - - - - - -
  41. -- T R A N S L A T I O N S
  42. -- - - - - - - - - - - - - - - - - - - - - - - -
  43. local gettext = dt.gettext
  44. gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
  45. local function _(msgid)
  46. return gettext.dgettext(MODULE_NAME, msgid)
  47. end
  48. -- - - - - - - - - - - - - - - - - - - - - - - -
  49. -- M A I N
  50. -- - - - - - - - - - - - - - - - - - - - - - - -
  51. -- alias dt.control.sleep to sleep
  52. local sleep = dt.control.sleep
  53. ---------------------------------------------------------------
  54. -- some helper methods to log information messages
  55. log.log_level(log.info) -- log.info or log.warn or log.debug
  56. local LogCurrentStep = ''
  57. local LogMajorNr = 0
  58. local LogMajorMax = 0
  59. local LogSummaryMessages = {}
  60. local function GetLogInfoText(text)
  61. return '[' .. LogMajorNr .. '/' .. LogMajorMax .. '] ' .. LogCurrentStep .. ': ' .. text
  62. end
  63. local function LogInfo(text)
  64. log.msg(log.info, GetLogInfoText(text))
  65. end
  66. local function LogScreen(text)
  67. log.msg(log.screen, text)
  68. end
  69. local function LogSummaryClear()
  70. for k, v in pairs(LogSummaryMessages) do
  71. LogSummaryMessages[k] = nil
  72. end
  73. end
  74. local function LogSummaryMessage(text)
  75. table.insert(LogSummaryMessages, GetLogInfoText(text))
  76. end
  77. -- ----------------------------------------------------
  78. local function set_published_tag ( email, image )
  79. if email == 'postie_markus_spring.info@markus-spring.de' then
  80. local tagnr = dt.tags.find('photography|published|blog')
  81. dt.tags.attach(tagnr,image)
  82. elseif email == 'postie_vhs_fotogruppe_reichenhall@markus-spring.de' then
  83. local tagnr = dt.tags.find('photography|published|vhs-fotogruppe')
  84. dt.tags.attach(tagnr,image)
  85. -- else
  86. -- local tagnr = dt.tags.find('photography|published|instagram')
  87. -- dt.tags.attach(tagnr,image)
  88. end
  89. local tagnr = dt.tags.find('LOCKED')
  90. dt.tags.attach(tagnr,image)
  91. end
  92. local function getPureFilename(path)
  93. return string.match(path, "([^/\\]+)$")
  94. end
  95. local function set_metadata_note ( tmp_exported, image )
  96. if image then
  97. local current_notes = image.notes or "" -- Retrieve existing notes or set as empty
  98. if current_notes ~= "" then
  99. current_notes = current_notes .. "; "
  100. end
  101. image.notes = current_notes .. 'published as ' .. getPureFilename(tmp_exported)
  102. end
  103. end
  104. local function set_rating_min_2 ( image )
  105. if image.rating < 2 then
  106. image.rating = 2
  107. end
  108. end
  109. local function get_executable( binaryname, binarystring )
  110. local binary = dt.preferences.read(MODULE_NAME, binaryname, "string")
  111. if binary == "" then
  112. dt.print(_( binarystring .. " executable not configured"))
  113. return
  114. end
  115. binary = df.sanitize_filename(binary)
  116. return binary
  117. end
  118. local function run_exiftool ( file, tmp_exported, flags )
  119. local exiftoolbinary = get_executable("exiftoolbinary", "exiftool")
  120. run_cmd = exiftoolbinary..' -TagsFromFile '..file..' '..flags..' '..tmp_exported
  121. -- LogInfo(string.format("Running %s", run_cmd))
  122. local job = dt.gui.create_job(string.format("Running %s", run_cmd), true, stop_job)
  123. resp = dtsys.external_command(run_cmd)
  124. job.valid = false
  125. end
  126. local function isempty(s)
  127. return s == nil or s == ''
  128. end
  129. local function remove_unneeded_exif_values ( jpeg_file )
  130. local tags_to_remove = {
  131. "ModifyDate", "DateTimeOriginal", "CreateDate", "DateCreated",
  132. "TimeCreated", "GPSTimeStamp", "GPSDateStamp", "GPSDateTime",
  133. "IFD0:*", "IFD1:*"
  134. }
  135. local args = {"-overwrite_original"}
  136. for _, tag in ipairs(tags_to_remove) do
  137. table.insert(args, "-" .. tag .. "=")
  138. end
  139. -- table.insert(args, "-FileModifyDate<FileModifyDate")
  140. table.insert(args, jpeg_file)
  141. local exiftoolbinary = get_executable("exiftoolbinary", "exiftool")
  142. run_cmd = exiftoolbinary..' '.. table.concat(args, " ")
  143. -- dt.print_log(string.format("Running %s", run_cmd))
  144. local job = dt.gui.create_job(string.format("Running %s", run_cmd), true, stop_job)
  145. resp = dtsys.external_command(run_cmd)
  146. job.valid = false
  147. return nil
  148. end
  149. -- Function to replace German umlauts and sanitize the filename
  150. local function create_safe_filename(title)
  151. local replacements = {
  152. ["ä"] = "ae", ["ö"] = "oe", ["ü"] = "ue",
  153. ["Ä"] = "Ae", ["Ö"] = "Oe", ["Ü"] = "Ue",
  154. ["ß"] = "ss"
  155. }
  156. title = title:gsub("[%z\1-\127\194-\244][\128-\191]*", function(char)
  157. return replacements[char] or char
  158. end)
  159. title = title:gsub("[^%w%-]", "_")
  160. title = title:gsub("_+", "_")
  161. title = title:gsub("^_+", ""):gsub("_+$", "")
  162. if title == "" then
  163. title = "untitled"
  164. end
  165. title = title:sub(1, 255)
  166. return title
  167. end
  168. -- ----------------------------------------------------
  169. dt.print_log(MODULE_NAME .. ' loaded')
  170. -- save the configuration
  171. local current_view = dt.gui.current_view()
  172. local select_publication_target = dt.new_widget("combobox")
  173. {
  174. label = "target",
  175. tooltip = "Select blog for image publication",
  176. changed_callback = function(w) dt.preferences.write(MODULE_NAME, "target", "string", w.selected) end,
  177. email_1, email_2, email_3
  178. }
  179. local function process_image(image_path)
  180. local exiftoolbinary = get_executable("exiftoolbinary", "exiftool")
  181. -- Get metadata using exiftool
  182. local metadata_cmd = exiftoolbinary .. " -j -n -XMP:HierarchicalSubject -Composite:GPSPosition -XMP:Title " .. image_path
  183. dt.print_log(metadata_cmd)
  184. local handle = io.popen(metadata_cmd)
  185. local json_data = handle:read("*a")
  186. handle:close()
  187. print(json_data)
  188. -- Parse JSON data using cjson
  189. local metadata = cjson.decode(json_data)[1]
  190. local first_item = metadata
  191. local h_subject = first_item.HierarchicalSubject or ""
  192. local gps_position = first_item.GPSPosition or ""
  193. local title = first_item.Title or ""
  194. -- find ^where|
  195. local function find_where_value(tbl)
  196. for _, value in pairs(tbl) do
  197. if type(value) == "string" and string.find(value, "^where|") then
  198. return value
  199. end
  200. end
  201. return nil -- Not found
  202. end
  203. where = find_where_value(h_subject)
  204. local platz = ""
  205. local ort = ""
  206. local platz_ort_table = {where:match(".*|(.-)|([^|]+)$")}
  207. for i, v in ipairs(platz_ort_table) do
  208. if i == 1 then
  209. platz = v
  210. elseif i == 2 then
  211. ort = v
  212. end
  213. end
  214. -- Process HierarchicalSubject
  215. local h_subject_list = {}
  216. for _, v in pairs(h_subject) do
  217. if not ( tostring(v):find("^darktable") or
  218. tostring(v):find("^photography|published") or
  219. tostring(v):find("^where|") or
  220. tostring(v):find("^LOCKED") ) then
  221. table.insert(h_subject_list, v)
  222. end
  223. end
  224. local lat, lon = gps_position:match("(-?[%d%.]+) (-?[%d%.]+)")
  225. local caption = string.format(
  226. "#img1 %s, %s (<a target='_blank' href='https://www.openstreetmap.org/?mlat=%s&mlon=%s#map=18/%s/%s'>OSM</a>)#",
  227. platz or "", ort or "", lat or "", lon or "", lat or "", lon or ""
  228. )
  229. -- Generate tags, extract the last part of each string and format it
  230. local function subject_tags(t)
  231. local result = {}
  232. for _, str in ipairs(t) do
  233. -- Find the last part of the string after the last '|'
  234. local lastPart = str:match(".*|([^|]+)$")
  235. -- Add it to the result table, wrapped in brackets
  236. table.insert(result, "[" .. lastPart .. "]")
  237. end
  238. -- Concatenate all elements into a single string
  239. return table.concat(result, " ")
  240. end
  241. tags = subject_tags(h_subject_list)
  242. title = tags .. " " .. title
  243. return filtered_subject, title, caption
  244. end
  245. local publication_button = dt.new_widget("button")
  246. {
  247. label = "Publish!",
  248. clicked_callback = function ()
  249. for i_, i in ipairs(dt.gui.action_images) do
  250. if isempty(i.title) then
  251. local job = dt.gui.create_job("FEHLER: Das Bild " .. i.filename .. " hat keinen Titel.", true, stop_job)
  252. os.execute("sleep " .. 3)
  253. job.valid = false
  254. else
  255. set_rating_min_2( i )
  256. -- create duplicate
  257. local newimg = i.duplicate_with_history(i)
  258. local jpeg_exporter = dt.new_format("jpeg")
  259. jpeg_exporter.max_height = image_max_y
  260. jpeg_exporter.max_width = image_max_x
  261. -- local tmp_exported = os.tmpname()..".jpg"
  262. local tempname = os.tmpname()
  263. os.remove(tempname)
  264. os.execute("mkdir " .. tempname)
  265. local tmp_exported = tempname .. '/' .. create_safe_filename(i.title) .. '.jpg'
  266. local job = dt.gui.create_job(string.format(_("Converting raw file '%s' to jpeg..."), i.filename), true, stop_job)
  267. jpeg_exporter:write_image(i, tmp_exported, false)
  268. -- copy exif data from original file
  269. run_exiftool( df.sanitize_filename(i.path..PS..i.filename), tmp_exported, '-exif:all --subifd:all --Orientation -overwrite_original' )
  270. -- copy exif data from xmp file
  271. run_exiftool( df.sanitize_filename(i.sidecar) , tmp_exported, '-xmp:all -exif:all --subifd:all -overwrite_original' )
  272. local h_subject, title, caption = process_image(tmp_exported)
  273. remove_unneeded_exif_values(tmp_exported)
  274. run_cmd = "thunderbird -compose \"to=".. select_publication_target.value ..",subject='" .. title .."',body='" .. caption .. "',attachment='file://" .. tmp_exported .."'\""
  275. job = false
  276. local job = dt.gui.create_job("Running " .. run_cmd, true, stop_job)
  277. os.execute(run_cmd)
  278. set_published_tag ( select_publication_target.value, i )
  279. job = false
  280. set_metadata_note ( tmp_exported, i )
  281. LogScreen("Done")
  282. end
  283. end
  284. end
  285. }
  286. local lib_widgets = {}
  287. table.insert(lib_widgets, select_publication_target)
  288. table.insert(lib_widgets, publication_button)
  289. -- ... and tell dt about it all
  290. dt.register_lib(
  291. MODULE_NAME, -- plugin name
  292. MODULE_NAME, -- name
  293. true, -- expandable
  294. false, -- resetable
  295. {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
  296. dt.new_widget("box") -- widget
  297. {
  298. orientation = "vertical",
  299. -- sensitive = enfuse_installed,
  300. table.unpack(lib_widgets)
  301. },
  302. nil,-- view_enter
  303. nil -- view_leave
  304. )
  305. -- end
  306. -- -- register the new preferences -----------------------------------------------
  307. dt.preferences.register(MODULE_NAME, "exiftoolbinary", "file",
  308. _(MODULE_NAME .. ": executable for exiftool"),
  309. _("select executable for exiftool command line version") , "")
  310. dt.preferences.register(MODULE_NAME, "spring2lifescript", "file",
  311. _(MODULE_NAME .. ": executable for mail2spring2life script"),
  312. _("select executable for mail2spring2life script") , "")
  313. -- ----------------------------------------------------------------------------------
  314. -- set the destroy routine so that script_manager can call it when
  315. -- it's time to destroy the script and then return the data to
  316. -- script_manager
  317. local script_data = {}
  318. script_data.destroy = destroy
  319. return script_data