mail2blog.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. -- print(where)
  205. -- extract platz, ort
  206. local platz = ""
  207. local ort = ""
  208. local platz_ort_table = {where:match(".*|(.-)|([^|]+)$")}
  209. for i, v in ipairs(platz_ort_table) do
  210. -- print(i .. ' ' .. v)
  211. if i == 1 then
  212. platz = v
  213. elseif i == 2 then
  214. ort = v
  215. end
  216. end
  217. -- print(platz)
  218. -- print(ort)
  219. -- Process HierarchicalSubject
  220. local h_subject_list = {}
  221. for _, v in pairs(h_subject) do
  222. if not ( tostring(v):find("^darktable") or
  223. tostring(v):find("^photography|published") or
  224. tostring(v):find("^where|") or
  225. tostring(v):find("^LOCKED") ) then
  226. table.insert(h_subject_list, v)
  227. end
  228. end
  229. -- Extract GPS coordinates
  230. local lat, lon = gps_position:match("([%d%.]+) ([%d%.]+)")
  231. -- Generate caption
  232. local caption = string.format(
  233. "#img1 %s, %s (<a target='_blank' href='https://www.openstreetmap.org/?mlat=%s&mlon=%s#map=18/%s/%s'>OSM</a>)#",
  234. platz or "", ort or "", lat or "", lon or "", lat or "", lon or ""
  235. )
  236. -- print(caption)
  237. -- Generate tags, extract the last part of each string and format it
  238. local function subject_tags(t)
  239. local result = {}
  240. for _, str in ipairs(t) do
  241. -- Find the last part of the string after the last '|'
  242. local lastPart = str:match(".*|([^|]+)$")
  243. -- Add it to the result table, wrapped in brackets
  244. table.insert(result, "[" .. lastPart .. "]")
  245. end
  246. -- Concatenate all elements into a single string
  247. return table.concat(result, " ")
  248. end
  249. tags = subject_tags(h_subject_list)
  250. -- Generate title
  251. title = tags .. " " .. title
  252. return filtered_subject, title, caption
  253. end
  254. local publication_button = dt.new_widget("button")
  255. {
  256. label = "Publish!",
  257. clicked_callback = function ()
  258. for i_, i in ipairs(dt.gui.action_images) do
  259. if isempty(i.title) then
  260. local job = dt.gui.create_job("FEHLER: Das Bild " .. i.filename .. " hat keinen Titel.", true, stop_job)
  261. os.execute("sleep " .. 3)
  262. job.valid = false
  263. else
  264. set_rating_min_2( i )
  265. -- create duplicate
  266. local newimg = i.duplicate_with_history(i)
  267. local jpeg_exporter = dt.new_format("jpeg")
  268. jpeg_exporter.max_height = image_max_y
  269. jpeg_exporter.max_width = image_max_x
  270. -- local tmp_exported = os.tmpname()..".jpg"
  271. local tempname = os.tmpname()
  272. os.remove(tempname)
  273. os.execute("mkdir " .. tempname)
  274. local tmp_exported = tempname .. '/' .. create_safe_filename(i.title) .. '.jpg'
  275. local job = dt.gui.create_job(string.format(_("Converting raw file '%s' to jpeg..."), i.filename), true, stop_job)
  276. jpeg_exporter:write_image(i, tmp_exported, false)
  277. -- copy exif data from original file
  278. run_exiftool( df.sanitize_filename(i.path..PS..i.filename), tmp_exported, '-exif:all --subifd:all --Orientation -overwrite_original' )
  279. -- copy exif data from xmp file
  280. run_exiftool( df.sanitize_filename(i.sidecar) , tmp_exported, '-xmp:all -exif:all --subifd:all -overwrite_original' )
  281. local h_subject, title, caption = process_image(tmp_exported)
  282. print("Title:", title)
  283. print("Caption:", caption)
  284. remove_unneeded_exif_values(tmp_exported)
  285. -- local spring2lifescript = get_executable("spring2lifescript", "spring2life script")
  286. -- run_cmd = spring2lifescript .. " " .. tmp_exported
  287. -- .. ' ' .. select_publication_target.value
  288. -- .. ' ' .. df.sanitize_filename(i.filename) .. ' &'
  289. run_cmd = "thunderbird -compose \"to=".. select_publication_target.value ..",subject='" .. title .."',body='" .. caption .. "',attachment='file://" .. tmp_exported .."'\""
  290. print(run_cmd)
  291. job = false
  292. local job = dt.gui.create_job("Running " .. run_cmd, true, stop_job)
  293. os.execute(run_cmd)
  294. set_published_tag ( select_publication_target.value, i )
  295. job = false
  296. set_metadata_note ( tmp_exported, i )
  297. LogScreen("Done")
  298. end
  299. end
  300. end
  301. }
  302. local lib_widgets = {}
  303. table.insert(lib_widgets, select_publication_target)
  304. table.insert(lib_widgets, publication_button)
  305. -- ... and tell dt about it all
  306. dt.register_lib(
  307. MODULE_NAME, -- plugin name
  308. MODULE_NAME, -- name
  309. true, -- expandable
  310. false, -- resetable
  311. {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
  312. dt.new_widget("box") -- widget
  313. {
  314. orientation = "vertical",
  315. -- sensitive = enfuse_installed,
  316. table.unpack(lib_widgets)
  317. },
  318. nil,-- view_enter
  319. nil -- view_leave
  320. )
  321. -- end
  322. -- -- register the new preferences -----------------------------------------------
  323. dt.preferences.register(MODULE_NAME, "exiftoolbinary", "file",
  324. _(MODULE_NAME .. ": executable for exiftool"),
  325. _("select executable for exiftool command line version") , "")
  326. dt.preferences.register(MODULE_NAME, "spring2lifescript", "file",
  327. _(MODULE_NAME .. ": executable for mail2spring2life script"),
  328. _("select executable for mail2spring2life script") , "")
  329. -- ----------------------------------------------------------------------------------
  330. -- set the destroy routine so that script_manager can call it when
  331. -- it's time to destroy the script and then return the data to
  332. -- script_manager
  333. local script_data = {}
  334. script_data.destroy = destroy
  335. return script_data