Skip to content

Commit 22e3c59

Browse files
authored
Merge pull request #611 from scorpi11/website_gallery
Website gallery replacement
2 parents 90d2764 + eb743e4 commit 22e3c59

File tree

9 files changed

+1069
-0
lines changed

9 files changed

+1069
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ passport_guide_germany|Yes|LMW|Add passport cropping guide for German passports
7979
[slideshowMusic](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow
8080
[transfer_hierarchy](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy
8181
[video_ffmpeg](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable
82+
website_gallery_export|No|LMW|Export a website gallery for selected images
8283

8384
### Example Scripts
8485

contrib/website_gallery_export.lua

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
--[[Export module to create a web gallery from selected images
2+
3+
copyright (c) 2025 Tino Mettler
4+
5+
darktable is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
darktable is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this software. If not, see <http://www.gnu.org/licenses/>.
17+
]]
18+
19+
--[[
20+
TODO:
21+
- Lua: remove images dir if already existent
22+
- Lua: translations
23+
]]
24+
25+
local dt = require "darktable"
26+
local df = require "lib/dtutils.file"
27+
28+
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
29+
30+
local temp = dt.preferences.read('web_gallery', 'title', 'string')
31+
if temp == nil then temp = 'Darktable gallery' end
32+
33+
local function _(msgid)
34+
return dt.gettext.gettext(msgid)
35+
end
36+
37+
local title_widget = dt.new_widget("entry")
38+
{
39+
text = temp
40+
}
41+
42+
local temp = dt.preferences.read('web_gallery', 'destination_dir', 'string')
43+
if temp == nil then temp = '' end
44+
45+
local dest_dir_widget = dt.new_widget("file_chooser_button")
46+
{
47+
title = _("select output folder"),
48+
tooltip = _("select output folder"),
49+
value = temp,
50+
is_directory = true,
51+
changed_callback = function(this) dt.preferences.write('web_gallery', 'destination_dir', 'string', this.value) end
52+
}
53+
54+
local gallery_widget = dt.new_widget("box")
55+
{
56+
orientation=vertical,
57+
dt.new_widget("label"){label = _("gallery title")},
58+
title_widget,
59+
dt.new_widget("label"){label = _("destination directory")},
60+
dest_dir_widget
61+
}
62+
63+
local function get_file_name(file)
64+
return file:match("[^" .. PS .. "]*.$")
65+
end
66+
67+
function escape_js_string(str)
68+
local replacements = {
69+
['\\'] = '\\\\',
70+
['"'] = '\\"',
71+
["'"] = "\\'",
72+
['\n'] = '\\n',
73+
['\r'] = '\\r',
74+
['\t'] = '\\t',
75+
['\b'] = '\\b',
76+
['\f'] = '\\f',
77+
['\v'] = '\\v'
78+
}
79+
return (str:gsub('[\\\"\n\r\t\b\f\v\']', replacements))
80+
end
81+
82+
local function export_thumbnail(image, filename)
83+
exporter = dt.new_format("jpeg")
84+
exporter.quality = 90
85+
exporter.max_height = 512
86+
exporter.max_width = 512
87+
exporter:write_image(image, filename, true)
88+
end
89+
90+
local function write_image(image, dest_dir, filename)
91+
df.file_move(filename, dest_dir.. PS .. "images" .. PS .. get_file_name(filename))
92+
export_thumbnail(image, dest_dir .. PS .. "thumbnails" .. PS .. "thumb_" .. get_file_name(filename))
93+
end
94+
95+
function exiftool_get_image_dimensions(filename)
96+
local handle = io.popen("exiftool " .. filename)
97+
local result = handle:read("*a")
98+
handle:close()
99+
for line in result:gmatch("[^\r\n]+") do
100+
local w = line:match("^Image Width%s*:%s*(%d+)")
101+
if w then
102+
width = tonumber(w)
103+
end
104+
local h = line:match("^Image Height%s*:%s*(%d+)")
105+
if h then
106+
height = tonumber(h)
107+
end
108+
end
109+
if width and height then
110+
return width, height
111+
else
112+
return nil, nil
113+
end
114+
end
115+
116+
local function stop_job(job)
117+
job.valid = false
118+
end
119+
120+
local function fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
121+
local gallery_data = { name = escape_js_string(title) }
122+
123+
local images = {}
124+
local index = 1
125+
local job = dt.gui.create_job(_("exporting thumbnail images"), true, stop_job)
126+
127+
for i, image in pairs(images_ordered) do
128+
local filename = df.sanitize_filename(images_table[image])
129+
dt.print(_("export thumbnail image ") .. index .. "/" .. #images_ordered)
130+
write_image(image, dest_dir, filename)
131+
132+
if exiftool then
133+
width, height = exiftool_get_image_dimensions(df.sanitize_filename(dest_dir .. PS .. "images" .. PS .. get_file_name(filename)))
134+
else
135+
width = sizes[index].width
136+
height = sizes[index].height
137+
end
138+
139+
local entry = { filename = "images" .. PS .. get_file_name(filename),
140+
width = width, height = height }
141+
142+
images[index] = entry
143+
job.percent = index / #images_ordered
144+
index = index + 1
145+
end
146+
147+
stop_job(job)
148+
gallery_data.images = images
149+
return gallery_data
150+
end
151+
152+
local function generate_javascript_gallery_object(gallery)
153+
local js = 'const gallery_data = {\n'
154+
js = js .. ' name: "' .. gallery.name .. '",\n'
155+
js = js .. ' images: [\n'
156+
157+
for i, img in ipairs(gallery.images) do
158+
js = js .. string.format(' { filename: "%s",\n height: %d,\n width: %d }', img.filename, img.height, img.width)
159+
if i < #gallery.images then
160+
js = js .. ',\n'
161+
else
162+
js = js .. '\n'
163+
end
164+
end
165+
166+
js = js .. ' ]\n};\n'
167+
168+
return(js)
169+
end
170+
171+
local function write_javascript_file(gallery_table, dest_dir)
172+
dt.print(_("write JavaScript file"))
173+
javascript_object = generate_javascript_gallery_object(gallery_table)
174+
175+
local fileOut, errr = io.open(dest_dir .. PS .. "js" .. PS .. "images.js", 'w+')
176+
if fileOut then
177+
fileOut:write(javascript_object)
178+
else
179+
log.msg(log.error, errr)
180+
end
181+
fileOut:close()
182+
end
183+
184+
local function copy_static_files(dest_dir)
185+
186+
gfsrc = dt.configuration.config_dir .. PS .. "lua" .. PS .. "data" .. PS .. "website_gallery"
187+
local gfiles = {
188+
"index.html",
189+
"css" .. PS .. "gallery.css",
190+
"css" .. PS .. "modal.css",
191+
"css" .. PS .. "style.css",
192+
"js" .. PS .. "gallery.js",
193+
"js" .. PS .. "modal.js",
194+
"js" .. PS .. "fullscreen.js"
195+
}
196+
197+
dt.print(_("copy static gallery files"))
198+
for _, file in ipairs(gfiles) do
199+
df.file_copy(gfsrc .. PS .. file, dest_dir .. PS .. file)
200+
end
201+
end
202+
203+
local function build_gallery(storage, images_table, extra_data)
204+
local dest_dir = dest_dir_widget.value
205+
df.mkdir(df.sanitize_filename(dest_dir))
206+
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "images"))
207+
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "thumbnails"))
208+
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "css"))
209+
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "js"))
210+
211+
local images_ordered = extra_data["images"] -- process images in the correct order
212+
local sizes = extra_data["sizes"]
213+
local title = _("Darktable export")
214+
if title_widget.text ~= "" then
215+
title = title_widget.text
216+
end
217+
local exiftool = df.check_if_bin_exists("exiftool");
218+
gallerydata = fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
219+
write_javascript_file(gallerydata, dest_dir)
220+
copy_static_files(dest_dir)
221+
end
222+
223+
local script_data = {}
224+
225+
script_data.metadata = {
226+
name = _("website gallery (new)"),
227+
purpose = _("create a web gallery from exported images"),
228+
author = "Tino Mettler <tino+darktable@tikei.de>",
229+
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/TODO"
230+
}
231+
232+
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
233+
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
234+
script_data.show = nil -- only required for libs since the destroy_method only hides them
235+
236+
local function destroy()
237+
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
238+
dt.destroy_storage("module_webgallery")
239+
end
240+
script_data.destroy = destroy
241+
242+
local function show_status(storage, image, format, filename,
243+
number, total, high_quality, extra_data)
244+
dt.print(string.format(_("export image ").."%i/%i", number, total))
245+
aspect = image.aspect_ratio
246+
-- calculate the size of the exported image and store it in extra_data
247+
-- to make it available in the finalize function
248+
if image.final_height == 0 then
249+
if aspect < 1 then
250+
dimensions = { width = image.height, height = image.width }
251+
else
252+
dimensions = { width = image.width, height = image.height }
253+
end
254+
else
255+
dimensions = { width = image.final_width, height = image.final_height }
256+
end
257+
if format.max_height > 0 and dimensions.height > format.max_height then
258+
scale = format.max_height / dimensions.height
259+
dimensions.height = math.floor(dimensions.height * scale + 0.5)
260+
dimensions.width = math.floor(dimensions.width * scale + 0.5)
261+
end
262+
if format.max_width > 0 and dimensions.width > format.max_width then
263+
scale = format.max_width / dimensions.width
264+
dimensions.height = math.floor(dimensions.height * scale + 0.5)
265+
dimensions.width = math.floor(dimensions.width * scale + 0.5)
266+
end
267+
extra_data["sizes"][number] = dimensions
268+
end
269+
270+
local function initialize(storage, img_format, images, high_quality, extra_data)
271+
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
272+
extra_data["images"] = images -- needed, to preserve images order
273+
extra_data["sizes"] = {};
274+
end
275+
276+
local supported_formats = { "jpg", "tif", "png", "webp" }
277+
278+
local formats_lut = {}
279+
for key,format in pairs(supported_formats) do
280+
formats_lut[format] = true
281+
end
282+
283+
function check_supported(storage, format)
284+
extension = format.extension
285+
if formats_lut[extension] == true then
286+
return true
287+
else
288+
return false
289+
end
290+
end
291+
292+
dt.register_storage("module_webgallery", "website gallery (new)", show_status, build_gallery, check_supported, initialize, gallery_widget)
293+
294+
return script_data
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
copyright (c) 2025 Tino Mettler
3+
4+
darktable is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
darktable is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this software. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
body {
19+
width: 100vw;
20+
height: 100vh;
21+
display: flex;
22+
flex-direction: column;
23+
}
24+
25+
.heading h1 {
26+
text-align: center;
27+
display: grid;
28+
}
29+
30+
.gallery {
31+
display: flex;
32+
flex-wrap: wrap;
33+
justify-content: center;
34+
align-items: center;
35+
}
36+
37+
.gallery img {
38+
cursor: pointer;
39+
object-fit: contain;
40+
}
41+
42+
.thumb {
43+
object-fit: contain;
44+
flex-shrink: 0;
45+
min-width: 100%;
46+
min-height: 100%;
47+
max-width: 100%;
48+
max-height: 100%;
49+
}
50+
51+
.thumb-box {
52+
display: flex;
53+
align-items: center;
54+
object-fit: contain;
55+
justify-content: center;
56+
}

0 commit comments

Comments
 (0)