Skip to content

Commit fa6014f

Browse files
author
Tino Mettler
committed
Export module for a website gallery with selected images
This export module should replace the PhotoSwipe copy in darktable. See darktable-org/darktable#16205 for more details.
1 parent e831f37 commit fa6014f

File tree

7 files changed

+1077
-0
lines changed

7 files changed

+1077
-0
lines changed

contrib/website_gallery_export.lua

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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+
- before PR: Zoom, export code from wpferguson, use share_dir
22+
- Lua: remove images dir if already existent
23+
- Lua: implement "supported" callback to limit export to suited file formats
24+
- Lua: translations
25+
]]
26+
27+
local dt = require "darktable"
28+
local df = require "lib/dtutils.file"
29+
30+
local temp = dt.preferences.read('web_gallery', 'title', 'string')
31+
if temp == nil then temp = 'Darktable gallery' end
32+
33+
local title_widget = dt.new_widget("entry")
34+
{
35+
text = temp
36+
}
37+
38+
local temp = dt.preferences.read('web_gallery', 'destination_dir', 'string')
39+
if temp == nil then temp = '' end
40+
41+
local dest_dir_widget = dt.new_widget("file_chooser_button")
42+
{
43+
title = "select output folder",
44+
tooltip = "select output folder",
45+
value = temp,
46+
is_directory = true,
47+
changed_callback = function(this) dt.preferences.write('web_gallery', 'destination_dir', 'string', this.value) end
48+
}
49+
50+
local gallery_widget = dt.new_widget("box")
51+
{
52+
orientation=vertical,
53+
dt.new_widget("label"){label = "gallery title"},
54+
title_widget,
55+
dt.new_widget("label"){label = "destination directory"},
56+
dest_dir_widget
57+
}
58+
59+
local function get_file_name(file)
60+
return file:match("[^/]*.$")
61+
end
62+
63+
function escape_js_string(str)
64+
local replacements = {
65+
['\\'] = '\\\\',
66+
['"'] = '\\"',
67+
["'"] = "\\'",
68+
['\n'] = '\\n',
69+
['\r'] = '\\r',
70+
['\t'] = '\\t',
71+
['\b'] = '\\b',
72+
['\f'] = '\\f',
73+
['\v'] = '\\v'
74+
}
75+
return (str:gsub('[\\\"\n\r\t\b\f\v\']', replacements))
76+
end
77+
78+
local function export_thumbnail(image, filename)
79+
dt.print("export thumbnail image "..filename)
80+
exporter = dt.new_format("jpeg")
81+
exporter.quality = 90
82+
exporter.max_height = 512
83+
exporter.max_width = 512
84+
exporter:write_image(image, filename, true)
85+
end
86+
87+
local function write_image(image, dest_dir, filename)
88+
df.file_move(filename, dest_dir.."/"..get_file_name(filename))
89+
export_thumbnail(image, dest_dir.."/thumb_"..get_file_name(filename))
90+
end
91+
92+
function exiftool_get_image_dimensions(filename)
93+
local handle = io.popen("exiftool " .. filename)
94+
local result = handle:read("*a")
95+
handle:close()
96+
for line in result:gmatch("[^\r\n]+") do
97+
local w = line:match("^Image Width%s*:%s*(%d+)")
98+
if w then
99+
width = tonumber(w)
100+
end
101+
local h = line:match("^Image Height%s*:%s*(%d+)")
102+
if h then
103+
height = tonumber(h)
104+
end
105+
end
106+
if width and height then
107+
return width, height
108+
else
109+
return nil, nil
110+
end
111+
end
112+
113+
local function fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
114+
dest_dir = dest_dir.."/images"
115+
local gallery_data = { name = escape_js_string(title) }
116+
117+
local images = {}
118+
local index = 1
119+
for i, image in pairs(images_ordered) do
120+
local filename = images_table[image]
121+
write_image(image, dest_dir, filename)
122+
123+
if exiftool then
124+
width, height = exiftool_get_image_dimensions(dest_dir.."/"..get_file_name(filename))
125+
else
126+
width = sizes[index].width
127+
height = sizes[index].height
128+
end
129+
130+
local entry = { filename = "images/"..get_file_name(escape_js_string(filename)),
131+
width = width, height = height }
132+
133+
images[index] = entry
134+
index = index + 1
135+
end
136+
137+
gallery_data.images = images
138+
return gallery_data
139+
end
140+
141+
local function generate_javascript_gallery_object(gallery)
142+
local js = 'const gallery_data = {\n'
143+
js = js .. ' name: "' .. gallery.name .. '",\n'
144+
js = js .. ' images: [\n'
145+
146+
for i, img in ipairs(gallery.images) do
147+
js = js .. string.format(' { filename: "%s",\n height: %d,\n width: %d }', img.filename, img.height, img.width)
148+
if i < #gallery.images then
149+
js = js .. ',\n'
150+
else
151+
js = js .. '\n'
152+
end
153+
end
154+
155+
js = js .. ' ]\n};\n'
156+
157+
return(js)
158+
end
159+
160+
local function write_javascript_file(gallery_table, dest_dir)
161+
dt.print("write JavaScript file")
162+
javascript_object = generate_javascript_gallery_object(gallery_table)
163+
164+
local fileOut, errr = io.open(dest_dir.."/images.js", 'w+')
165+
if fileOut then
166+
fileOut:write(javascript_object)
167+
else
168+
log.msg(log.error, errr)
169+
end
170+
fileOut:close()
171+
end
172+
173+
local function copy_static_files(dest_dir)
174+
dt.print("copy static gallery files")
175+
gfsrc = dt.configuration.config_dir.."/lua/data/website_gallery"
176+
gfiles = {
177+
"index.html",
178+
"gallery.css",
179+
"modal.css",
180+
"modal.js",
181+
"gallery.js",
182+
"fullscreen.js"
183+
}
184+
185+
for _, file in ipairs(gfiles) do
186+
df.file_copy(gfsrc.."/"..file, dest_dir.."/"..file)
187+
end
188+
end
189+
190+
local function build_gallery(storage, images_table, extra_data)
191+
local dest_dir = dest_dir_widget.value
192+
df.mkdir(dest_dir)
193+
df.mkdir(dest_dir.."/images")
194+
195+
local images_ordered = extra_data["images"] -- process images in the correct order
196+
local sizes = extra_data["sizes"]
197+
local title = "Darktable export"
198+
if title_widget.text ~= "" then
199+
title = title_widget.text
200+
end
201+
local exiftool = df.check_if_bin_exists("exiftool");
202+
gallerydata = fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
203+
write_javascript_file(gallerydata, dest_dir)
204+
copy_static_files(dest_dir)
205+
end
206+
207+
local script_data = {}
208+
209+
script_data.metadata = {
210+
name = "website gallery (new)",
211+
purpose = "create a web gallery from exported images",
212+
author = "Tino Mettler <tino+darktable@tikei.de>",
213+
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/TODO"
214+
}
215+
216+
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
217+
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
218+
script_data.show = nil -- only required for libs since the destroy_method only hides them
219+
220+
local function destroy()
221+
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
222+
dt.destroy_storage("module_webgallery")
223+
end
224+
script_data.destroy = destroy
225+
226+
local function show_status(storage, image, format, filename,
227+
number, total, high_quality, extra_data)
228+
dt.print(string.format("export image %i/%i", number, total))
229+
aspect = image.aspect_ratio
230+
-- calculate the size of the exported image and store it in extra_data
231+
-- to make it available in the finalize function
232+
if image.final_height == 0 then
233+
if aspect < 1 then
234+
dimensions = { width = image.height, height = image.width }
235+
else
236+
dimensions = { width = image.width, height = image.height }
237+
end
238+
else
239+
dimensions = { width = image.final_width, height = image.final_height }
240+
end
241+
if format.max_height > 0 and dimensions.height > format.max_height then
242+
scale = format.max_height / dimensions.height
243+
dimensions.height = math.floor(dimensions.height * scale + 0.5)
244+
dimensions.width = math.floor(dimensions.width * scale + 0.5)
245+
end
246+
if format.max_width > 0 and dimensions.width > format.max_width then
247+
scale = format.max_width / dimensions.width
248+
dimensions.height = math.floor(dimensions.height * scale + 0.5)
249+
dimensions.width = math.floor(dimensions.width * scale + 0.5)
250+
end
251+
extra_data["sizes"][number] = dimensions
252+
end
253+
254+
local function initialize(storage, img_format, images, high_quality, extra_data)
255+
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
256+
extra_data["images"] = images -- needed, to preserve images order
257+
extra_data["sizes"] = {};
258+
end
259+
260+
dt.register_storage("module_webgallery", "website gallery (new)", show_status, build_gallery, nil, initialize, gallery_widget)
261+
262+
return script_data

data/website_gallery/fullscreen.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
var isFullscreen = false;
2+
3+
var toggleFullscreen = function (ele) {
4+
return isFullscreen ? exitFullscreen(ele) : requestFullscreen(ele);
5+
};
6+
7+
var requestFullscreen = function (ele) {
8+
if(isFullscreen == true) return 0 ;
9+
10+
isFullscreen = true;
11+
if (ele.requestFullscreen) {
12+
ele.requestFullscreen();
13+
} else if (ele.webkitRequestFullscreen) {
14+
ele.webkitRequestFullscreen();
15+
} else if (ele.mozRequestFullScreen) {
16+
ele.mozRequestFullScreen();
17+
} else if (ele.msRequestFullscreen) {
18+
ele.msRequestFullscreen();
19+
} else {
20+
console.log('Fullscreen API is not supported.');
21+
}
22+
};
23+
24+
var exitFullscreen = function () {
25+
if(isFullscreen == false) return 0;
26+
27+
isFullscreen = false;
28+
if (document.exitFullscreen) {
29+
document.exitFullscreen();
30+
} else if (document.webkitExitFullscreen) {
31+
document.webkitExitFullscreen();
32+
} else if (document.mozCancelFullScreen) {
33+
document.mozCancelFullScreen();
34+
} else if (document.msExitFullscreen) {
35+
document.msExitFullscreen();
36+
} else {
37+
console.log('Fullscreen API is not supported.');
38+
}
39+
};
40+

0 commit comments

Comments
 (0)