Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ passport_guide_germany|Yes|LMW|Add passport cropping guide for German passports
[slideshowMusic](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow
[transfer_hierarchy](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy
[video_ffmpeg](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable
website_gallery_export|No|LMW|Export a website gallery for selected images

### Example Scripts

Expand Down
290 changes: 290 additions & 0 deletions contrib/website_gallery_export.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
--[[Export module to create a web gallery from selected images

copyright (c) 2025 Tino Mettler

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 this software. If not, see <http://www.gnu.org/licenses/>.
]]

--[[
TODO:
- Lua: remove images dir if already existent
- Lua: translations
]]

local dt = require "darktable"
local df = require "lib/dtutils.file"

local temp = dt.preferences.read('web_gallery', 'title', 'string')
if temp == nil then temp = 'Darktable gallery' end

local function _(msgid)
return dt.gettext.gettext(msgid)
end

local title_widget = dt.new_widget("entry")
{
text = temp
}

local temp = dt.preferences.read('web_gallery', 'destination_dir', 'string')
if temp == nil then temp = '' end

local dest_dir_widget = dt.new_widget("file_chooser_button")
{
title = _("select output folder"),
tooltip = _("select output folder"),
value = temp,
is_directory = true,
changed_callback = function(this) dt.preferences.write('web_gallery', 'destination_dir', 'string', this.value) end
}

local gallery_widget = dt.new_widget("box")
{
orientation=vertical,
dt.new_widget("label"){label = _("gallery title")},
title_widget,
dt.new_widget("label"){label = _("destination directory")},
dest_dir_widget
}

local function get_file_name(file)
return file:match("[^/]*.$")
end

function escape_js_string(str)
local replacements = {
['\\'] = '\\\\',
['"'] = '\\"',
["'"] = "\\'",
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
['\b'] = '\\b',
['\f'] = '\\f',
['\v'] = '\\v'
}
return (str:gsub('[\\\"\n\r\t\b\f\v\']', replacements))
end

local function export_thumbnail(image, filename)
exporter = dt.new_format("jpeg")
exporter.quality = 90
exporter.max_height = 512
exporter.max_width = 512
exporter:write_image(image, filename, true)
end

local function write_image(image, dest_dir, filename)
df.file_move(filename, dest_dir.."/images/"..get_file_name(filename))
export_thumbnail(image, dest_dir.."/thumbnails/thumb_"..get_file_name(filename))
end

function exiftool_get_image_dimensions(filename)
local handle = io.popen("exiftool " .. filename)
local result = handle:read("*a")
handle:close()
for line in result:gmatch("[^\r\n]+") do
local w = line:match("^Image Width%s*:%s*(%d+)")
if w then
width = tonumber(w)
end
local h = line:match("^Image Height%s*:%s*(%d+)")
if h then
height = tonumber(h)
end
end
if width and height then
return width, height
else
return nil, nil
end
end

local function stop_job(job)
job.valid = false
end

local function fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
local gallery_data = { name = escape_js_string(title) }

local images = {}
local index = 1
local job = dt.gui.create_job(_("exporting thumbnail images"), true, stop_job)

for i, image in pairs(images_ordered) do
local filename = images_table[image]
dt.print(_("exporting thumbnail image ")..index.."/"..#images_ordered)
write_image(image, dest_dir, filename)

if exiftool then
width, height = exiftool_get_image_dimensions(dest_dir.."/images/"..get_file_name(filename))
else
width = sizes[index].width
height = sizes[index].height
end

local entry = { filename = "images/"..get_file_name(escape_js_string(filename)),
width = width, height = height }

images[index] = entry
job.percent = index / #images_ordered
index = index + 1
end

stop_job(job)
gallery_data.images = images
return gallery_data
end

local function generate_javascript_gallery_object(gallery)
local js = 'const gallery_data = {\n'
js = js .. ' name: "' .. gallery.name .. '",\n'
js = js .. ' images: [\n'

for i, img in ipairs(gallery.images) do
js = js .. string.format(' { filename: "%s",\n height: %d,\n width: %d }', img.filename, img.height, img.width)
if i < #gallery.images then
js = js .. ',\n'
else
js = js .. '\n'
end
end

js = js .. ' ]\n};\n'

return(js)
end

local function write_javascript_file(gallery_table, dest_dir)
dt.print(_("write JavaScript file"))
javascript_object = generate_javascript_gallery_object(gallery_table)

local fileOut, errr = io.open(dest_dir.."/js/images.js", 'w+')
if fileOut then
fileOut:write(javascript_object)
else
log.msg(log.error, errr)
end
fileOut:close()
end

local function copy_static_files(dest_dir)
gfsrc = dt.configuration.config_dir.."/lua/data/website_gallery"
local gfiles = {
"index.html",
"css/gallery.css",
"css/modal.css",
"js/gallery.js",
"js/modal.js",
"js/fullscreen.js"
}

dt.print(_("copy static gallery files"))
for _, file in ipairs(gfiles) do
df.file_copy(gfsrc.."/"..file, dest_dir.."/"..file)
end
end

local function build_gallery(storage, images_table, extra_data)
local dest_dir = dest_dir_widget.value
df.mkdir(dest_dir)
df.mkdir(dest_dir.."/images")
df.mkdir(dest_dir.."/thumbnails")
df.mkdir(dest_dir.."/css")
df.mkdir(dest_dir.."/js")

local images_ordered = extra_data["images"] -- process images in the correct order
local sizes = extra_data["sizes"]
local title = _("Darktable export")
if title_widget.text ~= "" then
title = title_widget.text
end
local exiftool = df.check_if_bin_exists("exiftool");
gallerydata = fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
write_javascript_file(gallerydata, dest_dir)
copy_static_files(dest_dir)
end

local script_data = {}

script_data.metadata = {
name = "website gallery (new)",
purpose = "create a web gallery from exported images",
author = "Tino Mettler <[email protected]>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/TODO"
}

script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
script_data.show = nil -- only required for libs since the destroy_method only hides them

local function destroy()
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
dt.destroy_storage("module_webgallery")
end
script_data.destroy = destroy

local function show_status(storage, image, format, filename,
number, total, high_quality, extra_data)
dt.print(string.format(_("export image").."%i/%i", number, total))
aspect = image.aspect_ratio
-- calculate the size of the exported image and store it in extra_data
-- to make it available in the finalize function
if image.final_height == 0 then
if aspect < 1 then
dimensions = { width = image.height, height = image.width }
else
dimensions = { width = image.width, height = image.height }
end
else
dimensions = { width = image.final_width, height = image.final_height }
end
if format.max_height > 0 and dimensions.height > format.max_height then
scale = format.max_height / dimensions.height
dimensions.height = math.floor(dimensions.height * scale + 0.5)
dimensions.width = math.floor(dimensions.width * scale + 0.5)
end
if format.max_width > 0 and dimensions.width > format.max_width then
scale = format.max_width / dimensions.width
dimensions.height = math.floor(dimensions.height * scale + 0.5)
dimensions.width = math.floor(dimensions.width * scale + 0.5)
end
extra_data["sizes"][number] = dimensions
end

local function initialize(storage, img_format, images, high_quality, extra_data)
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
extra_data["images"] = images -- needed, to preserve images order
extra_data["sizes"] = {};
end

local supported_formats = { "jpg", "tif", "png", "webp" }

local formats_lut = {}
for key,format in pairs(supported_formats) do
formats_lut[format] = true
end

function check_supported(storage, format)
extension = format.extension
if formats_lut[extension] == true then
return true
else
return false
end
end

dt.register_storage("module_webgallery", "website gallery (new)", show_status, build_gallery, check_supported, initialize, gallery_widget)

return script_data
Loading