Skip to content

Commit 3df5039

Browse files
committed
Add contrib/fujifilm_auto_settings script
1 parent 35cbff0 commit 3df5039

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dbmaint|Yes|LMW|find and remove database entries for missing film rolls and imag
4848
[exportLUT](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/exportlut)|Yes|LMW|Create a LUT from a style and export it
4949
[ext_editor](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/ext_editor)|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors
5050
[face_recognition](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/face_recognition)|No|LM|Identify and tag images using facial recognition
51+
[fujifilm_auto_settings](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_auto_settings)|No|LMW|Apply fujifilm film emulations on import. [See here](https://bastibe.de/2022-05-04-customizing-darktable-for-fujifilm-cameras.html) for more information.
5152
[fujifilm_dynamic_range](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range)|No|LMW|Correct fujifilm exposure based on exposure bias camera setting
5253
[fujifilm_ratings](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_ratings)|No|LM|Support importing Fujifilm ratings
5354
[geoJSON_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/geojson_export)|No|L|Create a geo JSON script with thumbnails for use in ...

Diff for: contrib/fujifilm_auto_settings.lua

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
--[[ fujifilm_auto_settings-0.4
2+
3+
Apply Fujifilm film simulations, in-camera crop mode, and dynamic range.
4+
5+
Copyright (C) 2022 Bastian Bechtold <[email protected]>
6+
7+
This program is free software; you can redistribute it and/or modify
8+
it under the terms of the GNU General Public License as published by
9+
the Free Software Foundation; either version 2 of the License, or
10+
(at your option) any later version.
11+
12+
This program is distributed in the hope that it will be useful,
13+
but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
GNU General Public License for more details.
16+
17+
You should have received a copy of the GNU General Public License along
18+
with this program; if not, write to the Free Software Foundation, Inc.,
19+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20+
--]]
21+
22+
--[[About this Plugin
23+
Automatically applies styles that load Fujifilm film simulation LUTs,
24+
copy crop ratios from the JPG, and correct exposure according to the
25+
chosen dynamic range setting in camera.
26+
27+
Dependencies:
28+
- exiftool (https://www.sno.phy.queensu.ca/~phil/exiftool/)
29+
30+
Based on fujifim_dynamic_range by Dan Torop.
31+
32+
Film Simulations
33+
----------------
34+
35+
Fujifilm cameras are famous for their film simulations, such as Provia
36+
or Velvia or Classic Chrome. Indeed it is my experience that they rely
37+
on these film simulations for accurate colors.
38+
39+
Darktable however does not know about or implement these film
40+
simulations. But I created a set of LUTs that emulate them. The LUTs
41+
were created from a set of training images on an X-T3, and generated
42+
using https://github.com/bastibe/LUT-Maker.
43+
44+
In order to apply the film simulations, this plugin loads one of a
45+
number of styles, which are available for download as part of this
46+
repository:
47+
- provia
48+
- astia
49+
- velvia
50+
- classic_chrome
51+
- pro_neg_standard
52+
- pro_neg_high
53+
- eterna
54+
- acros_green
55+
- acros_red
56+
- acros_yellow
57+
- acros
58+
- mono_green
59+
- mono_red
60+
- mono_yellow
61+
- mono
62+
- sepia
63+
64+
These styles apply the chosen film simulation by loading one of the
65+
supplied LUTs. You can replace them with styles of the same name that
66+
implement the film simulations in some other way.
67+
68+
This plugin checks the image's "Film Mode" EXIF parameter for the
69+
color film simulation, and "Saturation" for the black-and-white film
70+
simulation, and applies the appropriate style. If no matching style
71+
exists, no action is taken and no harm is done.
72+
73+
Crop Factor
74+
-----------
75+
76+
Fujifilm cameras allow in-camera cropping to one of three aspect
77+
ratios: 2:3 (default), 16:9, and 1:1.
78+
79+
This plugin checks the image's "Raw Image Aspect Ratio" exif
80+
parameter, and applies the appropriate style.
81+
82+
To use, the repository contains another set of styles:
83+
- square_crop_portrait
84+
- square_crop_landscape
85+
- sixteen_by_nine_crop_portrait
86+
- sixteen_by_nine_crop_landscape
87+
88+
These styles should apply a square crop and a 16:9 crop. If no
89+
matching style exists, no action is taken and no harm is done.
90+
91+
Dynamic Range
92+
-------------
93+
94+
Fujifilm cameras have a built-in dynamic range compensation, which
95+
(optionally automatically) reduce exposure by one or two stops, and
96+
compensate by raising the tone curve by one or two stops. These modes
97+
are called DR200 and DR400, respectively.
98+
99+
The plugin reads the raw file's "Auto Dynamic Range" or "Development
100+
Dynamic Range" parameter, and applies one of two styles:
101+
- DR200
102+
- DR400
103+
104+
These styles should raise exposure by one and two stops, respectively,
105+
and expand highlight latitude to make room for additional highlights.
106+
I like to implement them with the tone equalizer in eigf mode, raising
107+
exposure by one/two stops over the lower half of the sliders, then
108+
ramping to zero at 0 EV. If no matching styles exist, no action is
109+
taken and no harm is done.
110+
111+
These tags have been checked on a Fujifilm X-A5, X-T3, X-T20 and
112+
X-Pro2. Other cameras may behave in other ways.
113+
114+
--]]
115+
116+
local dt = require("darktable")
117+
local du = require("lib/dtutils")
118+
local df = require("lib/dtutils.file")
119+
120+
du.check_min_api_version("7.0.0", "fujifilm_auto_settings")
121+
122+
-- return data structure for script_manager
123+
124+
local script_data = {}
125+
126+
script_data.destroy = nil -- function to destory the script
127+
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them completely yet, otherwise leave as nil
128+
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
129+
130+
local function exiftool(RAF_filename)
131+
local exiftool_command = df.check_if_bin_exists("exiftool")
132+
assert(exiftool_command, "[fujifilm_auto_settings] exiftool not found")
133+
local command = exiftool_command .. " -t " .. RAF_filename
134+
-- on Windows, wrap the command in another pair of quotes:
135+
if exiftool_command:find(".exe") then
136+
command = '"' .. command .. '"'
137+
end
138+
dt.print_log("[fujifilm_auto_settings] executing " .. command)
139+
140+
-- parse the output of exiftool into a table:
141+
local output = io.popen(command)
142+
local exifdata = {}
143+
for line in output:lines("l") do
144+
local key, value = line:match("^%s*(.-)\t(.-)%s*$")
145+
if key ~= nil and value ~= nil then
146+
exifdata[key] = value
147+
end
148+
end
149+
output:close()
150+
151+
assert(next(exifdata) ~= nil, "[fujifilm_auto_settings] no output returned by exiftool")
152+
return exifdata
153+
end
154+
155+
local function apply_style(image, style_name)
156+
for _, s in ipairs(dt.styles) do
157+
if s.name == style_name then
158+
dt.styles.apply(s, image)
159+
return
160+
end
161+
end
162+
dt.print_error("[fujifilm_auto_settings] could not find style " .. style_name)
163+
end
164+
165+
local function apply_tag(image, tag_name)
166+
local tagnum = dt.tags.find(tag_name)
167+
if tagnum == nil then
168+
-- create tag if it doesn't exist
169+
tagnum = dt.tags.create(tag_name)
170+
dt.print_log("[fujifilm_auto_settings] creating tag " .. tag_name)
171+
end
172+
dt.tags.attach(tagnum, image)
173+
end
174+
175+
local function detect_auto_settings(event, image)
176+
if image.exif_maker ~= "FUJIFILM" then
177+
dt.print_log("[fujifilm_auto_settings] ignoring non-Fujifilm image")
178+
return
179+
end
180+
-- it would be nice to check image.is_raw but this appears to not yet be set
181+
if not string.match(image.filename, "%.RAF$") then
182+
dt.print_log("[fujifilm_auto_settings] ignoring non-raw image")
183+
return
184+
end
185+
local RAF_filename = df.sanitize_filename(tostring(image))
186+
187+
local exifdata = exiftool(RAF_filename)
188+
189+
-- dynamic range mode
190+
-- if in DR Auto, the value is saved to Auto Dynamic Range, with a % suffix:
191+
local auto_dynamic_range = exifdata["Auto Dynamic Range"]
192+
-- if manually chosen DR, the value is saved to Development Dynamic Range:
193+
if auto_dynamic_range == nil then
194+
auto_dynamic_range = exifdata["Development Dynamic Range"] .. "%"
195+
end
196+
if auto_dynamic_range == "100%" then
197+
apply_tag(image, "DR100")
198+
-- default; no need to change style
199+
elseif auto_dynamic_range == "200%" then
200+
apply_style(image, "DR200")
201+
apply_tag(image, "DR200")
202+
dt.print_log("[fujifilm_auto_settings] applying DR200")
203+
elseif auto_dynamic_range == "400%" then
204+
apply_style(image, "DR400")
205+
apply_tag(image, "DR400")
206+
dt.print_log("[fujifilm_auto_settings] applying DR400")
207+
end
208+
209+
-- cropmode
210+
local raw_aspect_ratio = exifdata["Raw Image Aspect Ratio"]
211+
local raw_orientation = exifdata["Orientation"]
212+
if raw_aspect_ratio == "3:2" then
213+
apply_tag(image, "3:2")
214+
-- default; no need to apply style
215+
elseif raw_aspect_ratio == "1:1" then
216+
if raw_orientation == "Horizontal (normal)" or raw_orientation == "Rotate 180" then
217+
apply_style(image, "square_crop_landscape")
218+
else
219+
apply_style(image, "square_crop_portrait")
220+
end
221+
apply_tag(image, "1:1")
222+
dt.print_log("[fujifilm_auto_settings] applying square crop")
223+
elseif raw_aspect_ratio == "16:9" then
224+
if raw_orientation == "Horizontal (normal)" or raw_orientation == "Rotate 180" then
225+
apply_style(image, "sixteen_by_nine_crop_landscape")
226+
else
227+
apply_style(image, "sixteen_by_nine_crop_portrait")
228+
end
229+
apply_tag(image, "16:9")
230+
dt.print_log("[fujifilm_auto_settings] applying 16:9 crop")
231+
end
232+
233+
-- filmmode
234+
local raw_filmmode = exifdata["Film Mode"]
235+
local raw_saturation = exifdata["Saturation"]
236+
-- Check if it's a color film mode
237+
if raw_filmmode then
238+
local style_map = {
239+
["Provia"] = "provia",
240+
["Astia"] = "astia",
241+
["Classic Chrome"] = "classic_chrome",
242+
["Eterna"] = "eterna",
243+
["Pro Neg. Hi"] = "pro_neg_high",
244+
["Pro Neg. Std"] = "pro_neg_standard",
245+
["Velvia"] = "velvia",
246+
}
247+
for key, value in pairs(style_map) do
248+
if string.find(raw_filmmode, key) then
249+
apply_style(image, value)
250+
apply_tag(image, key)
251+
dt.print_log("[fujifilm_auto_settings] applying film simulation " .. key)
252+
break
253+
end
254+
end
255+
-- else check if it's a b&w film mode
256+
elseif raw_saturation then
257+
local style_map = {
258+
["Acros Green Filter"] = "acros_green",
259+
["Acros Red Filter"] = "acros_red",
260+
["Acros Yellow Filter"] = "acros_yellow",
261+
["Acros"] = "acros",
262+
["None (B&W)"] = "mono",
263+
["B&W Green Filter"] = "mono_green",
264+
["B&W Red Filter"] = "mono_red",
265+
["B&W Yellow Filter"] = "mono_yellow",
266+
["B&W Sepia"] = "sepia",
267+
}
268+
for key, value in pairs(style_map) do
269+
if raw_saturation == key then
270+
apply_style(image, value)
271+
apply_tag(image, key)
272+
dt.print_log("[fujifilm_auto_settings] applying B&W film simulation " .. key)
273+
break
274+
end
275+
end
276+
else
277+
dt.print_log("[fujifilm_auto_settings] neither Film Mode or Saturation EXIF info was found")
278+
end
279+
end
280+
281+
local function detect_auto_settings_multi(event, shortcut)
282+
local images = dt.gui.selection()
283+
if #images == 0 then
284+
dt.print(_("Please select an image"))
285+
else
286+
for _, image in ipairs(images) do
287+
detect_auto_settings(event, image)
288+
end
289+
end
290+
end
291+
292+
local function destroy()
293+
dt.destroy_event("fujifilm_auto_settings", "post-import-image")
294+
dt.destroy_event("fujifilm_auto_settings", "shortcut")
295+
end
296+
297+
if not df.check_if_bin_exists("exiftool") then
298+
dt.print_log("Please install exiftool to use fujifilm_auto_settings")
299+
error("[fujifilm_auto_settings] exiftool not found")
300+
end
301+
302+
dt.register_event("fujifilm_auto_settings", "post-import-image", detect_auto_settings)
303+
304+
dt.register_event("fujifilm_auto_settings", "shortcut", detect_auto_settings_multi, "fujifilm_auto_settings")
305+
306+
dt.print_log("[fujifilm_auto_settings] loaded")
307+
308+
script_data.destroy = destroy
309+
310+
return script_data

0 commit comments

Comments
 (0)