Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d109612045 | ||
|
|
0f47f46b35 | ||
|
|
1a44df7d3c | ||
|
|
aa62473cbf | ||
|
|
f7faad0962 | ||
|
|
53549f16b5 | ||
|
|
7531a29235 | ||
|
|
a4dce23ef8 | ||
|
|
8abc350d68 | ||
|
|
078011d6b4 | ||
|
|
2a16599f91 | ||
|
|
14646cf8e6 | ||
|
|
5823d015ac | ||
|
|
30e6b9459a | ||
|
|
d3e15065d2 | ||
|
|
fe722f8409 | ||
|
|
b91baebdeb |
@@ -138,8 +138,9 @@ script_data.show = nil -- only required for libs since the destroy_method only h
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
local PS = dt.configuration.runnin_os == "windows" and "\\" or "/"
|
local PS <const> = dt.configuration.runnin_os == "windows" and "\\" or "/"
|
||||||
local ERROR = -1
|
local ERROR <const> = -1
|
||||||
|
local HAS_INTERNAL_METADATA = dt.metadata.exists("Exif.Image.DataTime")
|
||||||
|
|
||||||
-- function to convert from exif time to system time
|
-- function to convert from exif time to system time
|
||||||
local function exiftime2systime(exiftime)
|
local function exiftime2systime(exiftime)
|
||||||
@@ -241,15 +242,16 @@ end
|
|||||||
|
|
||||||
local function _get_windows_image_file_creation_time(image)
|
local function _get_windows_image_file_creation_time(image)
|
||||||
local datetime = nil
|
local datetime = nil
|
||||||
|
local datestring = nil
|
||||||
|
|
||||||
|
if HAS_INTERNAL_METADATA then
|
||||||
|
datestring = image.DateString
|
||||||
|
else
|
||||||
local p = io.popen("dir " .. image.path .. PS .. image.filename)
|
local p = io.popen("dir " .. image.path .. PS .. image.filename)
|
||||||
if p then
|
if p then
|
||||||
for line in p:lines() do
|
for line in p:lines() do
|
||||||
if string.match(line, ds.sanitize_lua(image.filename)) then
|
if string.match(line, ds.sanitize_lua(image.filename)) then
|
||||||
local mo, day, yr, hr, min, apm = string.match(line, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)")
|
datestring = line
|
||||||
if apm == "PM" then
|
|
||||||
hr = hr + 12
|
|
||||||
end
|
|
||||||
datetime = vars2exiftime(yr, mo, day, hr, min, 0)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
p:close()
|
p:close()
|
||||||
@@ -257,6 +259,14 @@ local function _get_windows_image_file_creation_time(image)
|
|||||||
dt.print(string.format(_("unable to get information for %s"), image.filename))
|
dt.print(string.format(_("unable to get information for %s"), image.filename))
|
||||||
datetime = ERROR
|
datetime = ERROR
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
if datestring then
|
||||||
|
local mo, day, yr, hr, min, apm = string.match(datestring, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)")
|
||||||
|
if apm == "PM" then
|
||||||
|
hr = hr + 12
|
||||||
|
end
|
||||||
|
datetime = vars2exiftime(yr, mo, day, hr, min, 0)
|
||||||
|
end
|
||||||
return datetime
|
return datetime
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
323
examples/ai_denoise.lua
Normal file
323
examples/ai_denoise.lua
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
--[[
|
||||||
|
This file is part of darktable,
|
||||||
|
copyright (c) 2026 darktable developers.
|
||||||
|
|
||||||
|
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 darktable. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
--[[
|
||||||
|
ai_denoise - example script demonstrating the darktable.ai Lua
|
||||||
|
API by denoising the selected images with tile-based inference.
|
||||||
|
|
||||||
|
Adds an "AI denoise" panel to the lighttable right column with a
|
||||||
|
"denoise selected" button. Each selected image is loaded through
|
||||||
|
the develop pipeline (so denoise sees the output of all enabled
|
||||||
|
IOPs, not the original raw), run tile-by-tile through the active
|
||||||
|
denoise model, and saved as a 16-bit TIFF grouped with the source.
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
* model lookup via dt.ai.model_for_task
|
||||||
|
* tensor crop/paste for tile-based inference
|
||||||
|
* linear <-> sRGB conversion (NR models train on gamma input)
|
||||||
|
* edge-replicated padding to suppress border artifacts
|
||||||
|
* background job with progress bar and cancellation
|
||||||
|
|
||||||
|
LIMITATIONS
|
||||||
|
* Image must be at least TILE_SIZE x TILE_SIZE (768 x 768 for
|
||||||
|
denoise-nind / denoise-nafnet); smaller inputs return an error.
|
||||||
|
* If a save fails mid-batch the Lua error propagates and aborts
|
||||||
|
the whole job. Wrap denoise_one in pcall if you need
|
||||||
|
batch-robust behaviour.
|
||||||
|
|
||||||
|
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||||
|
* a denoise model enabled in preferences -> AI (denoise-nind,
|
||||||
|
denoise-nafnet, ...)
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
* require this script from your luarc
|
||||||
|
* select one or more images in lighttable
|
||||||
|
* click the "denoise selected" button in the AI denoise panel
|
||||||
|
|
||||||
|
BUGS, COMMENTS, SUGGESTIONS
|
||||||
|
* file an issue on https://github.com/darktable-org/darktable
|
||||||
|
|
||||||
|
CHANGES
|
||||||
|
]]
|
||||||
|
|
||||||
|
local dt = require "darktable"
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- C O N S T A N T S
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
local MODULE_NAME = "ai_denoise"
|
||||||
|
|
||||||
|
-- darktable's NR models use static-shape ONNX: each model has a fixed
|
||||||
|
-- input dimension baked in at training time. The Lua API doesn't
|
||||||
|
-- expose this, so keep TILE_SIZE in sync with the model's
|
||||||
|
-- `attributes.input_sizes` in its config.json.
|
||||||
|
local TILE_SIZE = 768 -- denoise-nind, denoise-nafnet
|
||||||
|
|
||||||
|
-- overlap between adjacent tiles. larger values produce smoother
|
||||||
|
-- seams at the cost of more compute per pixel
|
||||||
|
local OVERLAP = 64
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- F U N C T I O N S
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
-- Surround the input with `pad` pixels on every side, replicating the
|
||||||
|
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
|
||||||
|
-- see plausible context instead of a literal image edge, so the
|
||||||
|
-- network's internal zero-padding no longer leaks into the visible
|
||||||
|
-- output as colour artifacts along the image rim.
|
||||||
|
local function pad_replicate(input, pad)
|
||||||
|
local C = input:shape()[2]
|
||||||
|
local H = input:shape()[3]
|
||||||
|
local W = input:shape()[4]
|
||||||
|
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
|
||||||
|
|
||||||
|
-- center: original image
|
||||||
|
padded:paste(input, pad, pad)
|
||||||
|
|
||||||
|
-- left and right strips: replicate first / last column of input
|
||||||
|
local left_col = input:crop(0, 0, H, 1)
|
||||||
|
local right_col = input:crop(0, W - 1, H, 1)
|
||||||
|
for i = 0, pad - 1 do
|
||||||
|
padded:paste(left_col, pad, i)
|
||||||
|
padded:paste(right_col, pad, pad + W + i)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- top and bottom strips: replicate first / last row of the already
|
||||||
|
-- horizontally-padded buffer so the four corners get filled correctly
|
||||||
|
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
|
||||||
|
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
|
||||||
|
for i = 0, pad - 1 do
|
||||||
|
padded:paste(top_row, i, 0)
|
||||||
|
padded:paste(bottom_row, pad + H + i, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return padded
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tile a [1,C,H,W] tensor through the model with overlap-blended seams.
|
||||||
|
-- Each tile is tile_size x tile_size, successive tiles step by
|
||||||
|
-- (tile_size - 2*overlap), and only the central (non-overlap) region
|
||||||
|
-- of each tile is pasted into the output. Image-boundary edges keep
|
||||||
|
-- their full extent; only image-interior seams drop the overlap margin.
|
||||||
|
--
|
||||||
|
-- The trailing tile in each axis is pinned to the image boundary so
|
||||||
|
-- the residual right/bottom strip is never smaller than 2*overlap --
|
||||||
|
-- otherwise it would have to be skipped (the model has a fixed input
|
||||||
|
-- size) and those pixels would never be written.
|
||||||
|
local function run_tiled(ctx, input, tile_size, overlap,
|
||||||
|
job, base_pct, span_pct)
|
||||||
|
local C = input:shape()[2]
|
||||||
|
local H = input:shape()[3]
|
||||||
|
local W = input:shape()[4]
|
||||||
|
if H < tile_size or W < tile_size then
|
||||||
|
return nil, string.format("image too small (%dx%d) for tile %d",
|
||||||
|
W, H, tile_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
local step = tile_size - 2 * overlap
|
||||||
|
|
||||||
|
-- enumerate tile origins along one axis, ending exactly at the boundary
|
||||||
|
local function origins(extent)
|
||||||
|
local out, p = {}, 0
|
||||||
|
while p + tile_size < extent do
|
||||||
|
out[#out + 1] = p
|
||||||
|
p = p + step
|
||||||
|
end
|
||||||
|
out[#out + 1] = math.max(0, extent - tile_size)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local ys, xs = origins(H), origins(W)
|
||||||
|
local total = #xs * #ys
|
||||||
|
|
||||||
|
local output = dt.ai.create_tensor({1, C, H, W})
|
||||||
|
local done = 0
|
||||||
|
|
||||||
|
for _, ty in ipairs(ys) do
|
||||||
|
for _, tx in ipairs(xs) do
|
||||||
|
if job and not job.valid then return nil, "cancelled" end
|
||||||
|
|
||||||
|
local tile_in = input:crop(ty, tx, tile_size, tile_size)
|
||||||
|
local tile_out = ctx:run(tile_in)
|
||||||
|
|
||||||
|
-- central region to keep: drop overlap on every inner edge;
|
||||||
|
-- full extent at image-boundary edges
|
||||||
|
local at_top = (ty == 0)
|
||||||
|
local at_left = (tx == 0)
|
||||||
|
local at_bottom = (ty + tile_size >= H)
|
||||||
|
local at_right = (tx + tile_size >= W)
|
||||||
|
|
||||||
|
local cy = at_top and 0 or overlap
|
||||||
|
local cx = at_left and 0 or overlap
|
||||||
|
local ch = tile_size - cy - (at_bottom and 0 or overlap)
|
||||||
|
local cw = tile_size - cx - (at_right and 0 or overlap)
|
||||||
|
|
||||||
|
output:paste(tile_out:crop(cy, cx, ch, cw), ty + cy, tx + cx)
|
||||||
|
|
||||||
|
done = done + 1
|
||||||
|
if job then
|
||||||
|
job.percent = base_pct + span_pct * (done / total)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Denoise one image: load through the develop pipeline, run tiled
|
||||||
|
-- inference, save as 16-bit TIFF, group with the source.
|
||||||
|
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
|
||||||
|
-- darktable returns the develop-pipeline output as scene-linear RGB;
|
||||||
|
-- the NR models were trained on sRGB-gamma input, so we re-encode
|
||||||
|
-- before inference and decode back to linear before saving
|
||||||
|
local input = dt.ai.load_image(img)
|
||||||
|
if not input then return false, "load_image failed" end
|
||||||
|
input:linear_to_srgb()
|
||||||
|
|
||||||
|
-- pad so the image's outer rim is no longer at a tile boundary;
|
||||||
|
-- the padded strip absorbs the network's edge artifacts and gets
|
||||||
|
-- cropped away after inference
|
||||||
|
local H = input:shape()[3]
|
||||||
|
local W = input:shape()[4]
|
||||||
|
local padded_in = pad_replicate(input, OVERLAP)
|
||||||
|
|
||||||
|
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
|
||||||
|
job, base_pct, span_pct)
|
||||||
|
if not padded_out then return false, err end
|
||||||
|
|
||||||
|
local output = padded_out:crop(OVERLAP, OVERLAP, H, W)
|
||||||
|
output:srgb_to_linear()
|
||||||
|
|
||||||
|
local base = img.filename:match("(.+)%..+$") or img.filename
|
||||||
|
local out_path = img.path .. "/" .. base .. "_denoised.tif"
|
||||||
|
output:save_tiff(out_path, 16, img)
|
||||||
|
|
||||||
|
-- import the result and group with the source so they live together
|
||||||
|
-- in the lighttable
|
||||||
|
local imported = dt.database.import(out_path)
|
||||||
|
if imported then imported:group_with(img) end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Button handler: looks up the active denoise model, fans out the
|
||||||
|
-- selected images, and reports progress via a cancellable job.
|
||||||
|
local function process_denoise()
|
||||||
|
local model_id = dt.ai.model_for_task("denoise")
|
||||||
|
if not model_id then
|
||||||
|
dt.print("no denoise model enabled -- pick one in preferences -> AI")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local images = dt.gui.selection()
|
||||||
|
if #images == 0 then
|
||||||
|
dt.print("select at least one image")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- progress + cancel job. the cancel callback flips job.valid; the
|
||||||
|
-- main loop polls it between tiles to stop cleanly
|
||||||
|
local job = dt.gui.create_job(
|
||||||
|
string.format("AI denoise (%d image%s)",
|
||||||
|
#images, #images == 1 and "" or "s"),
|
||||||
|
true,
|
||||||
|
function(j) j.valid = false end)
|
||||||
|
|
||||||
|
dt.print_log(string.format(
|
||||||
|
"[ai_denoise] starting: %d image%s with model %s",
|
||||||
|
#images, #images == 1 and "" or "s", model_id))
|
||||||
|
local t_start = os.time()
|
||||||
|
|
||||||
|
local ctx = dt.ai.load_model(model_id)
|
||||||
|
if not ctx then
|
||||||
|
dt.print("failed to load model: " .. model_id)
|
||||||
|
job.valid = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local span = 1.0 / #images
|
||||||
|
local done = 0
|
||||||
|
for i, img in ipairs(images) do
|
||||||
|
if not job.valid then break end
|
||||||
|
dt.print_log(string.format("[ai_denoise] [%d/%d] %s",
|
||||||
|
i, #images, img.filename))
|
||||||
|
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
|
||||||
|
(i - 1) * span, span)
|
||||||
|
if not ok then
|
||||||
|
dt.print(string.format("[%d/%d] %s: %s",
|
||||||
|
i, #images, img.filename, tostring(err)))
|
||||||
|
dt.print_log(string.format("[ai_denoise] [%d/%d] %s FAILED: %s",
|
||||||
|
i, #images, img.filename, tostring(err)))
|
||||||
|
else
|
||||||
|
done = done + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ctx:close()
|
||||||
|
job.valid = false
|
||||||
|
local elapsed = os.time() - t_start
|
||||||
|
dt.print(string.format("denoise complete: %d image%s",
|
||||||
|
#images, #images == 1 and "" or "s"))
|
||||||
|
dt.print_log(string.format(
|
||||||
|
"[ai_denoise] finished: %d/%d in %ds",
|
||||||
|
done, #images, elapsed))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- script_manager integration: called when the user disables the script.
|
||||||
|
-- darktable can't remove a registered lib from the UI at runtime, so we
|
||||||
|
-- only stop any in-flight work; the panel itself remains until restart.
|
||||||
|
local function destroy()
|
||||||
|
-- nothing persistent to clean up: each click runs its own job and
|
||||||
|
-- closes its own model context
|
||||||
|
end
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- M A I N
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
dt.register_lib(
|
||||||
|
MODULE_NAME, -- plugin id
|
||||||
|
"AI denoise", -- displayed name
|
||||||
|
true, -- expandable
|
||||||
|
false, -- no reset button
|
||||||
|
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
|
||||||
|
dt.new_widget("box") {
|
||||||
|
orientation = "vertical",
|
||||||
|
dt.new_widget("button") {
|
||||||
|
label = "denoise selected",
|
||||||
|
clicked_callback = process_denoise,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nil, nil
|
||||||
|
)
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- S C R I P T M A N A G E R I N T E G R A T I O N
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
local script_data = {}
|
||||||
|
|
||||||
|
script_data.metadata = {
|
||||||
|
name = "AI denoise",
|
||||||
|
purpose = "tile-based AI denoise using the darktable.ai Lua API",
|
||||||
|
author = "Andrii Ryzhkov",
|
||||||
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_denoise"
|
||||||
|
}
|
||||||
|
|
||||||
|
script_data.destroy = destroy
|
||||||
|
|
||||||
|
return script_data
|
||||||
481
examples/ai_raw_denoise.lua
Normal file
481
examples/ai_raw_denoise.lua
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
--[[
|
||||||
|
This file is part of darktable,
|
||||||
|
copyright (c) 2026 darktable developers.
|
||||||
|
|
||||||
|
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 darktable. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
--[[
|
||||||
|
ai_raw_denoise - example script demonstrating the darktable.ai Lua
|
||||||
|
API by denoising the selected raw images with tile-based inference
|
||||||
|
on the raw CFA.
|
||||||
|
|
||||||
|
Adds an "AI raw denoise" panel to the lighttable right column with
|
||||||
|
a "denoise selected" button. Each selected image's raw CFA is
|
||||||
|
loaded, Bayer-packed to 4 channels, run tile-by-tile through the
|
||||||
|
currently enabled rawdenoise model (which denoises AND demosaicks
|
||||||
|
in one pass), and saved as a LinearRaw DNG grouped with the
|
||||||
|
source image.
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
* model lookup via dt.ai.model_for_task
|
||||||
|
* loading model variant files via dt.ai.load_model file arg
|
||||||
|
* raw CFA loading via dt.ai.load_raw
|
||||||
|
* tensor bayer_pack for 4-channel CFA tiling
|
||||||
|
* tile loop with non-trivial input->output scale (here 1 -> 2)
|
||||||
|
* edge-replicated padding to suppress border artifacts
|
||||||
|
* LinearRaw DNG output via dt.ai.save_dng_linear
|
||||||
|
* background job with progress bar and cancellation
|
||||||
|
|
||||||
|
LIMITATIONS
|
||||||
|
* Bayer sensors only. The script loads model_bayer.onnx and uses
|
||||||
|
bayer_pack, which assumes a 2x2 Bayer layout. X-Trans and
|
||||||
|
Foveon sensors need model_linear.onnx with a different
|
||||||
|
preprocessing pipeline (no bayer_pack, demosaicked input).
|
||||||
|
* Image must be at least TILE_SIZE x TILE_SIZE in packed pixels
|
||||||
|
(1024 x 1024 of CFA for the default 512 packed tile size).
|
||||||
|
* If a save fails mid-batch the Lua error propagates and aborts
|
||||||
|
the whole job. Wrap denoise_one in pcall if you need
|
||||||
|
batch-robust behaviour.
|
||||||
|
|
||||||
|
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||||
|
* a rawdenoise model enabled in preferences -> AI
|
||||||
|
(e.g. rawdenoise-nind)
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
* require this script from your luarc
|
||||||
|
* select one or more raw images in lighttable
|
||||||
|
* click the "denoise selected" button in the AI raw denoise panel
|
||||||
|
|
||||||
|
BUGS, COMMENTS, SUGGESTIONS
|
||||||
|
* file an issue on https://github.com/darktable-org/darktable
|
||||||
|
|
||||||
|
CHANGES
|
||||||
|
]]
|
||||||
|
|
||||||
|
local dt = require "darktable"
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- C O N S T A N T S
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
local MODULE_NAME = "ai_raw_denoise"
|
||||||
|
|
||||||
|
-- The rawdenoise-nind bayer model is a denoise+demosaic combo:
|
||||||
|
-- input : [1, 4, 512, 512] packed CFA (4 phase planes)
|
||||||
|
-- output : [1, 3, 1024, 1024] linear RGB at 2x the input resolution
|
||||||
|
-- So each tile-size pixel of the input covers a 2x2 CFA block, and the
|
||||||
|
-- model emits two output pixels per input pixel in each axis.
|
||||||
|
-- TILE_SIZE is in packed pixels (model input); SCALE turns those into
|
||||||
|
-- output pixels. Keep in sync with `attributes.input_sizes` in the
|
||||||
|
-- model's config.json.
|
||||||
|
local TILE_SIZE = 512 -- rawdenoise-nind
|
||||||
|
local SCALE = 2 -- model output dim / input dim
|
||||||
|
local OUT_CH = 3 -- model emits linear RGB
|
||||||
|
|
||||||
|
-- overlap between adjacent tiles, in packed pixels (input space).
|
||||||
|
-- larger values produce smoother seams at the cost of more compute
|
||||||
|
local OVERLAP = 32
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- F U N C T I O N S
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
-- Bayer-only CFA channel lookup. Returns 0/1/2 (R/G/B) for the cell
|
||||||
|
-- at sensor position (r, c) given the 32-bit `filters` mask from
|
||||||
|
-- load_raw's meta table. Used to find the R-cell origin so bayer_pack's
|
||||||
|
-- plane 0 always holds R. X-Trans (filters == 9) uses meta.xtrans
|
||||||
|
-- instead and is not handled here.
|
||||||
|
local function fc(r, c, filters)
|
||||||
|
local shift = ((r % 2) * 4) + ((c % 2) * 2)
|
||||||
|
return math.floor(filters / (2 ^ shift)) % 4
|
||||||
|
end
|
||||||
|
|
||||||
|
-- daylight WB multipliers (R, G=1, B) derived from the camera's
|
||||||
|
-- XYZ->camRGB color matrix and the D65 white point. mirrors the C
|
||||||
|
-- path's _bayer_wb_daylight in restore_raw_bayer.c — the rawdenoise
|
||||||
|
-- bayer model is trained on daylight-WB'd input, so feeding it
|
||||||
|
-- un-WB'd camRGB produces wrong colors. Falls back to {1, 1, 1} if
|
||||||
|
-- the matrix is missing or degenerate.
|
||||||
|
local function daylight_wb(color_matrix)
|
||||||
|
local d65 = {0.95047, 1.0, 1.08883}
|
||||||
|
if not color_matrix then return 1.0, 1.0, 1.0 end
|
||||||
|
local resp = {0, 0, 0}
|
||||||
|
for c = 1, 3 do
|
||||||
|
resp[c] = color_matrix[c][1] * d65[1]
|
||||||
|
+ color_matrix[c][2] * d65[2]
|
||||||
|
+ color_matrix[c][3] * d65[3]
|
||||||
|
end
|
||||||
|
if resp[1] <= 0 or resp[2] <= 0 or resp[3] <= 0 then
|
||||||
|
return 1.0, 1.0, 1.0
|
||||||
|
end
|
||||||
|
return resp[2] / resp[1], 1.0, resp[2] / resp[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Surround the input with `pad` pixels on every side, replicating the
|
||||||
|
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
|
||||||
|
-- see plausible context instead of a literal image edge, so the
|
||||||
|
-- network's internal zero-padding no longer leaks into the visible
|
||||||
|
-- output as colour artifacts along the image rim.
|
||||||
|
local function pad_replicate(input, pad)
|
||||||
|
local C = input:shape()[2]
|
||||||
|
local H = input:shape()[3]
|
||||||
|
local W = input:shape()[4]
|
||||||
|
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
|
||||||
|
|
||||||
|
-- center: original image
|
||||||
|
padded:paste(input, pad, pad)
|
||||||
|
|
||||||
|
-- left and right strips: replicate first / last column of input
|
||||||
|
local left_col = input:crop(0, 0, H, 1)
|
||||||
|
local right_col = input:crop(0, W - 1, H, 1)
|
||||||
|
for i = 0, pad - 1 do
|
||||||
|
padded:paste(left_col, pad, i)
|
||||||
|
padded:paste(right_col, pad, pad + W + i)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- top and bottom strips: replicate first / last row of the already
|
||||||
|
-- horizontally-padded buffer so the four corners get filled correctly
|
||||||
|
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
|
||||||
|
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
|
||||||
|
for i = 0, pad - 1 do
|
||||||
|
padded:paste(top_row, i, 0)
|
||||||
|
padded:paste(bottom_row, pad + H + i, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return padded
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tile a [1,C_in,H,W] tensor through a model that emits [1,C_out,H*scale,
|
||||||
|
-- W*scale]. Each tile is tile_size x tile_size in input space; successive
|
||||||
|
-- tiles step by (tile_size - 2*overlap); only the central (non-overlap)
|
||||||
|
-- region of each tile's output is pasted into the result, with all
|
||||||
|
-- offsets multiplied by `scale`. Image-boundary edges keep their full
|
||||||
|
-- extent; only image-interior seams drop the overlap margin.
|
||||||
|
--
|
||||||
|
-- The trailing tile in each axis is pinned to the image boundary so
|
||||||
|
-- the residual right/bottom strip is never smaller than 2*overlap --
|
||||||
|
-- otherwise it would have to be skipped (the model has a fixed input
|
||||||
|
-- size) and those pixels would never be written.
|
||||||
|
local function run_tiled(ctx, input, tile_size, overlap, scale, c_out,
|
||||||
|
job, base_pct, span_pct)
|
||||||
|
local H = input:shape()[3]
|
||||||
|
local W = input:shape()[4]
|
||||||
|
if H < tile_size or W < tile_size then
|
||||||
|
return nil, string.format("image too small (%dx%d) for tile %d",
|
||||||
|
W, H, tile_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
local step = tile_size - 2 * overlap
|
||||||
|
|
||||||
|
-- enumerate tile origins along one axis, ending exactly at the boundary
|
||||||
|
local function origins(extent)
|
||||||
|
local out, p = {}, 0
|
||||||
|
while p + tile_size < extent do
|
||||||
|
out[#out + 1] = p
|
||||||
|
p = p + step
|
||||||
|
end
|
||||||
|
out[#out + 1] = math.max(0, extent - tile_size)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local ys, xs = origins(H), origins(W)
|
||||||
|
local total = #xs * #ys
|
||||||
|
|
||||||
|
-- output is scale-times larger spatially and may have a different
|
||||||
|
-- channel count than the input
|
||||||
|
local output = dt.ai.create_tensor({1, c_out, H * scale, W * scale})
|
||||||
|
local done = 0
|
||||||
|
|
||||||
|
for _, ty in ipairs(ys) do
|
||||||
|
for _, tx in ipairs(xs) do
|
||||||
|
if job and not job.valid then return nil, "cancelled" end
|
||||||
|
|
||||||
|
local tile_in = input:crop(ty, tx, tile_size, tile_size)
|
||||||
|
local tile_out = ctx:run(tile_in)
|
||||||
|
|
||||||
|
-- central region to keep: drop overlap on every inner edge;
|
||||||
|
-- full extent at image-boundary edges. all dimensions in OUTPUT
|
||||||
|
-- (post-scale) space, since that's where we paste
|
||||||
|
local at_top = (ty == 0)
|
||||||
|
local at_left = (tx == 0)
|
||||||
|
local at_bottom = (ty + tile_size >= H)
|
||||||
|
local at_right = (tx + tile_size >= W)
|
||||||
|
|
||||||
|
local ov = overlap * scale
|
||||||
|
local tos = tile_size * scale
|
||||||
|
local cy = at_top and 0 or ov
|
||||||
|
local cx = at_left and 0 or ov
|
||||||
|
local ch = tos - cy - (at_bottom and 0 or ov)
|
||||||
|
local cw = tos - cx - (at_right and 0 or ov)
|
||||||
|
|
||||||
|
output:paste(tile_out:crop(cy, cx, ch, cw),
|
||||||
|
ty * scale + cy, tx * scale + cx)
|
||||||
|
|
||||||
|
done = done + 1
|
||||||
|
if job then
|
||||||
|
job.percent = base_pct + span_pct * (done / total)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Denoise one raw image. The bayer rawdenoise model denoises AND
|
||||||
|
-- demosaicks in one pass, so its output is linear RGB at the
|
||||||
|
-- original CFA resolution (not a CFA mosaic). Saved as a LinearRaw
|
||||||
|
-- DNG, which darktable can re-import as an already-debayered raw.
|
||||||
|
--
|
||||||
|
-- Pipeline:
|
||||||
|
-- load CFA [1,1,H,W] + image metadata
|
||||||
|
-- -> bayer_pack [1,4,H/2,W/2]
|
||||||
|
-- -> normalize to [0, 1] using black/white levels (the model is
|
||||||
|
-- trained on (val - black) / (white - black), not raw ADC)
|
||||||
|
-- -> pad to [1,4,H/2+2P,W/2+2P]
|
||||||
|
-- -> tile through model (scale=2, c_out=3) -> [1,3,(H+4P),(W+4P)]
|
||||||
|
-- -> crop padding back off -> [1,3,H,W]
|
||||||
|
-- -> save linear DNG -> import + group with source
|
||||||
|
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
|
||||||
|
-- load_raw returns (tensor, meta). tensor is [1,1,H,W] in raw ADC
|
||||||
|
-- units; meta carries black/white/wb/CFA info we need to normalise
|
||||||
|
-- the input into the [0, 1] range the model was trained on
|
||||||
|
local cfa, meta = dt.ai.load_raw(img)
|
||||||
|
if not cfa then return false, "load_raw failed" end
|
||||||
|
|
||||||
|
-- this script is Bayer-only: filters == 9 is X-Trans (handled by a
|
||||||
|
-- different preprocessing pipeline + model_linear.onnx); filters == 0
|
||||||
|
-- means monochrome / non-CFA, which bayer_pack can't handle either
|
||||||
|
if not meta.filters or meta.filters == 0 or meta.filters == 9 then
|
||||||
|
return false, "unsupported CFA pattern (Bayer-only script)"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Crop to the visible region and shift the origin onto the R cell
|
||||||
|
-- of the CFA pattern. bayer_pack splits by position only, so to make
|
||||||
|
-- plane 0 always hold R (which the model expects) the cropped CFA's
|
||||||
|
-- (0, 0) must sit at an R site. Mirrors the C path's _bayer_origin
|
||||||
|
-- + RGGB shift in restore_raw_bayer.c.
|
||||||
|
local cy = 0
|
||||||
|
local cx = 0
|
||||||
|
local cw = cfa:shape()[4]
|
||||||
|
local ch = cfa:shape()[3]
|
||||||
|
if meta.crop_x and meta.crop_y then
|
||||||
|
cy = meta.crop_y - (meta.crop_y % 2)
|
||||||
|
cx = meta.crop_x - (meta.crop_x % 2)
|
||||||
|
cw = (meta.visible_width or cw) + (meta.crop_x - cx)
|
||||||
|
ch = (meta.visible_height or ch) + (meta.crop_y - cy)
|
||||||
|
end
|
||||||
|
-- find R cell within the 2x2 starting at (cy, cx)
|
||||||
|
local sy, sx = 0, 0
|
||||||
|
for ty = 0, 1 do
|
||||||
|
for tx = 0, 1 do
|
||||||
|
if fc(cy + ty, cx + tx, meta.filters) == 0 then
|
||||||
|
sy, sx = ty, tx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cy = cy + sy
|
||||||
|
cx = cx + sx
|
||||||
|
ch = ch - sy
|
||||||
|
cw = cw - sx
|
||||||
|
-- ensure even dims for bayer_pack
|
||||||
|
ch = ch - (ch % 2)
|
||||||
|
cw = cw - (cw % 2)
|
||||||
|
if cy > 0 or cx > 0
|
||||||
|
or cw < cfa:shape()[4] or ch < cfa:shape()[3]
|
||||||
|
then
|
||||||
|
cfa = cfa:crop(cy, cx, ch, cw)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- uniform-black approximation: most sensors report identical black
|
||||||
|
-- levels across the 4 CFA sites, in which case a single scalar
|
||||||
|
-- (val - black) / (white - black) normalisation is exact. cameras
|
||||||
|
-- with non-uniform per-site black levels (e.g. some PDAF sensors)
|
||||||
|
-- would need per-plane scaling via scale_add_planes -- left as an
|
||||||
|
-- exercise
|
||||||
|
local black = meta.black_level
|
||||||
|
local range = meta.white_level - black
|
||||||
|
if range <= 0 then range = 65535 end
|
||||||
|
|
||||||
|
-- pack the 1-channel CFA into 4 phase planes so each channel holds
|
||||||
|
-- one Bayer site type. After the RGGB shift above:
|
||||||
|
-- plane 0 = R, planes 1,2 = G, plane 3 = B
|
||||||
|
local packed = cfa:bayer_pack()
|
||||||
|
packed:scale_add(1.0 / range, -black / range)
|
||||||
|
|
||||||
|
-- apply daylight WB so the four planes sit in the same range the
|
||||||
|
-- model was trained on (R, G, G, B → R*wb_R, G, G, B*wb_B). without
|
||||||
|
-- this, the model sees unbalanced channels and produces wrong colors
|
||||||
|
local wb_R, wb_G, wb_B = daylight_wb(meta.color_matrix)
|
||||||
|
packed:scale_add_planes({wb_R, wb_G, wb_G, wb_B})
|
||||||
|
local pH = packed:shape()[3]
|
||||||
|
local pW = packed:shape()[4]
|
||||||
|
|
||||||
|
-- save the post-normalisation input mean for gain-matching the
|
||||||
|
-- model output: the bayer rawdenoise model emits values on its own
|
||||||
|
-- internal scale, not [0, 1], so we rescale so output mean matches
|
||||||
|
-- input mean (the per-tile equivalent of dt's C reference path)
|
||||||
|
local in_mean = packed:mean()
|
||||||
|
|
||||||
|
-- pad so the image rim is no longer at a tile boundary; the padded
|
||||||
|
-- strip absorbs the network's edge artifacts and is cropped away
|
||||||
|
-- after inference (in output space, where the pad covers 2*OVERLAP
|
||||||
|
-- pixels per side)
|
||||||
|
local padded_in = pad_replicate(packed, OVERLAP)
|
||||||
|
|
||||||
|
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
|
||||||
|
SCALE, OUT_CH,
|
||||||
|
job, base_pct, span_pct)
|
||||||
|
if not padded_out then return false, err end
|
||||||
|
|
||||||
|
-- crop the padded region (in output coords: pad * scale per side)
|
||||||
|
-- back to the original-CFA resolution
|
||||||
|
local out_rgb = padded_out:crop(OVERLAP * SCALE, OVERLAP * SCALE,
|
||||||
|
pH * SCALE, pW * SCALE)
|
||||||
|
|
||||||
|
-- gain-match: rescale the output so its mean matches the input
|
||||||
|
-- mean, bringing the model's internal-scale output back into the
|
||||||
|
-- [0, 1] range save_dng_linear expects
|
||||||
|
local out_mean = out_rgb:mean()
|
||||||
|
if out_mean > 1e-8 then
|
||||||
|
out_rgb:scale_add(in_mean / out_mean)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- invert the daylight WB so the saved tensor is un-WB'd camRGB
|
||||||
|
-- (save_dng_linear's contract — the consumer applies AsShotNeutral
|
||||||
|
-- on import)
|
||||||
|
out_rgb:scale_add_planes({1.0 / wb_R, 1.0 / wb_G, 1.0 / wb_B})
|
||||||
|
|
||||||
|
local base = img.filename:match("(.+)%..+$") or img.filename
|
||||||
|
local out_path = img.path .. "/" .. base .. "_rawdenoised.dng"
|
||||||
|
-- LinearRaw DNG: tensor values must be normalised to [0, 1] camRGB;
|
||||||
|
-- the model handles black-subtract + scale internally, so we just
|
||||||
|
-- pass the output through
|
||||||
|
dt.ai.save_dng_linear(out_rgb, img, out_path)
|
||||||
|
|
||||||
|
-- import the result and group with the source so they live together
|
||||||
|
-- in the lighttable
|
||||||
|
local imported = dt.database.import(out_path)
|
||||||
|
if imported then imported:group_with(img) end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Button handler: looks up the active rawdenoise model, fans out the
|
||||||
|
-- selected images, and reports progress via a cancellable job.
|
||||||
|
local function process_raw_denoise()
|
||||||
|
local model_id = dt.ai.model_for_task("rawdenoise")
|
||||||
|
if not model_id then
|
||||||
|
dt.print("no rawdenoise model enabled -- pick one in preferences -> AI")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local images = dt.gui.selection()
|
||||||
|
if #images == 0 then
|
||||||
|
dt.print("select at least one image")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- progress + cancel job. the cancel callback flips job.valid; the
|
||||||
|
-- main loop polls it between tiles to stop cleanly
|
||||||
|
local job = dt.gui.create_job(
|
||||||
|
string.format("AI raw denoise (%d image%s)",
|
||||||
|
#images, #images == 1 and "" or "s"),
|
||||||
|
true,
|
||||||
|
function(j) j.valid = false end)
|
||||||
|
|
||||||
|
dt.print_log(string.format(
|
||||||
|
"[ai_raw_denoise] starting: %d image%s with model %s",
|
||||||
|
#images, #images == 1 and "" or "s", model_id))
|
||||||
|
local t_start = os.time()
|
||||||
|
|
||||||
|
-- rawdenoise packages ship multiple files (one per CFA type); pick
|
||||||
|
-- the Bayer variant. X-Trans / Foveon would use model_linear.onnx
|
||||||
|
-- with a different preprocessing pipeline (no bayer_pack)
|
||||||
|
local ctx = dt.ai.load_model(model_id, nil, "model_bayer.onnx")
|
||||||
|
if not ctx then
|
||||||
|
dt.print("failed to load model: " .. model_id)
|
||||||
|
job.valid = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local span = 1.0 / #images
|
||||||
|
local done = 0
|
||||||
|
for i, img in ipairs(images) do
|
||||||
|
if not job.valid then break end
|
||||||
|
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s",
|
||||||
|
i, #images, img.filename))
|
||||||
|
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
|
||||||
|
(i - 1) * span, span)
|
||||||
|
if not ok then
|
||||||
|
dt.print(string.format("[%d/%d] %s: %s",
|
||||||
|
i, #images, img.filename, tostring(err)))
|
||||||
|
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s FAILED: %s",
|
||||||
|
i, #images, img.filename, tostring(err)))
|
||||||
|
else
|
||||||
|
done = done + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ctx:close()
|
||||||
|
job.valid = false
|
||||||
|
local elapsed = os.time() - t_start
|
||||||
|
dt.print(string.format("raw denoise complete: %d image%s",
|
||||||
|
#images, #images == 1 and "" or "s"))
|
||||||
|
dt.print_log(string.format(
|
||||||
|
"[ai_raw_denoise] finished: %d/%d in %ds",
|
||||||
|
done, #images, elapsed))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- script_manager integration: called when the user disables the script.
|
||||||
|
-- darktable can't remove a registered lib from the UI at runtime, so we
|
||||||
|
-- only stop any in-flight work; the panel itself remains until restart.
|
||||||
|
local function destroy()
|
||||||
|
-- nothing persistent to clean up: each click runs its own job and
|
||||||
|
-- closes its own model context
|
||||||
|
end
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- M A I N
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
dt.register_lib(
|
||||||
|
MODULE_NAME, -- plugin id
|
||||||
|
"AI raw denoise", -- displayed name
|
||||||
|
true, -- expandable
|
||||||
|
false, -- no reset button
|
||||||
|
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
|
||||||
|
dt.new_widget("box") {
|
||||||
|
orientation = "vertical",
|
||||||
|
dt.new_widget("button") {
|
||||||
|
label = "denoise selected",
|
||||||
|
clicked_callback = process_raw_denoise,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nil, nil
|
||||||
|
)
|
||||||
|
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
-- S C R I P T M A N A G E R I N T E G R A T I O N
|
||||||
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
local script_data = {}
|
||||||
|
|
||||||
|
script_data.metadata = {
|
||||||
|
name = "AI raw denoise",
|
||||||
|
purpose = "tile-based AI raw denoise using the darktable.ai Lua API",
|
||||||
|
author = "Andrii Ryzhkov",
|
||||||
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_raw_denoise"
|
||||||
|
}
|
||||||
|
|
||||||
|
script_data.destroy = destroy
|
||||||
|
|
||||||
|
return script_data
|
||||||
@@ -211,9 +211,14 @@ local function straighten_image(image)
|
|||||||
for line in pipe:lines() do
|
for line in pipe:lines() do
|
||||||
local pr, val = string.match(line, "(.).+: (.+)")
|
local pr, val = string.match(line, "(.).+: (.+)")
|
||||||
if pr == "R" then
|
if pr == "R" then
|
||||||
|
local sign = 1.0
|
||||||
dt.print_log("roll angle is " .. val)
|
dt.print_log("roll angle is " .. val)
|
||||||
log.msg(log.debug, "roll angle is " .. val)
|
log.msg(log.debug, "roll angle is " .. val)
|
||||||
roll = determine_correction(tonumber(val))
|
-- check for Nidon and change the sign
|
||||||
|
if string.match(string.upper(image.exif_maker), "NIKON") then
|
||||||
|
sign = -1.0
|
||||||
|
end
|
||||||
|
roll = determine_correction(tonumber(val) * sign)
|
||||||
dt.print_log("corrected roll angle is " .. roll)
|
dt.print_log("corrected roll angle is " .. roll)
|
||||||
log.msg(log.debug, "corrected roll angle is " .. roll)
|
log.msg(log.debug, "corrected roll angle is " .. roll)
|
||||||
elseif pr == "P" then
|
elseif pr == "P" then
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
|
|||||||
local EXIFTOOL <const> = df.check_if_bin_exists("exiftool")
|
local EXIFTOOL <const> = df.check_if_bin_exists("exiftool")
|
||||||
local EXIV2 <const> = df.check_if_bin_exists("exiv2")
|
local EXIV2 <const> = df.check_if_bin_exists("exiv2")
|
||||||
local DNGLAB <const> = df.check_if_bin_exists("dnglab")
|
local DNGLAB <const> = df.check_if_bin_exists("dnglab")
|
||||||
|
local EXIV2TAG <const> = "Exif.Canon.RawBurstModeRoll"
|
||||||
|
|
||||||
|
local HAS_INTERNAL_METADATA <const> = dt.metadata.exists(EXIV2TAG)
|
||||||
|
|
||||||
local DNGLAB_ARGS <const> = " convert --image-index all --embed-raw false "
|
local DNGLAB_ARGS <const> = " convert --image-index all --embed-raw false "
|
||||||
local EXIV2_ARGS <const> = " -K Exif.Canon.RawBurstModeRoll -pt "
|
local EXIV2_ARGS <const> = " -K Exif.Canon.RawBurstModeRoll -pt "
|
||||||
@@ -150,6 +153,7 @@ local extract_burst_roll_images = {}
|
|||||||
|
|
||||||
extract_burst_roll_images.preferred_tag_reader = nil
|
extract_burst_roll_images.preferred_tag_reader = nil
|
||||||
extract_burst_roll_images.on_import = true
|
extract_burst_roll_images.on_import = true
|
||||||
|
extract_burst_roll_images.select_button = true
|
||||||
extract_burst_roll_images.imported_images = {}
|
extract_burst_roll_images.imported_images = {}
|
||||||
extract_burst_roll_images.log_level = DEFAULT_LOG_LEVEL
|
extract_burst_roll_images.log_level = DEFAULT_LOG_LEVEL
|
||||||
|
|
||||||
@@ -172,9 +176,18 @@ dt.preferences.register(MODULE, -- script: This is a string used to avoid
|
|||||||
"bool", -- type
|
"bool", -- type
|
||||||
_("extract burst roll images on import"), -- label
|
_("extract burst roll images on import"), -- label
|
||||||
_("extract burst roll images on import"), -- tooltip
|
_("extract burst roll images on import"), -- tooltip
|
||||||
|
false) -- default
|
||||||
|
|
||||||
|
-- display selection button
|
||||||
|
dt.preferences.register(MODULE, -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference.
|
||||||
|
"select_button", -- name
|
||||||
|
"bool", -- type
|
||||||
|
_("display select burst roll images button"), -- label
|
||||||
|
_("add a button to the select module to select burst roll images"), -- tooltip
|
||||||
true) -- default
|
true) -- default
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- A L I A S E S
|
-- A L I A S E S
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
@@ -256,6 +269,18 @@ end
|
|||||||
local function is_burst_roll_image(image)
|
local function is_burst_roll_image(image)
|
||||||
local old_log_level = set_log_level()
|
local old_log_level = set_log_level()
|
||||||
local result = false
|
local result = false
|
||||||
|
|
||||||
|
if HAS_INTERNAL_METADATA then
|
||||||
|
if image.RawBurstModeRoll then
|
||||||
|
local val = get_last_value(image.RawBurstModeRoll)
|
||||||
|
val = tonumber(val)
|
||||||
|
if type(val) == "number" then
|
||||||
|
if val > 0 then
|
||||||
|
result = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
local cmd = EXIFTOOL .. EXIFTOOL_ARGS
|
local cmd = EXIFTOOL .. EXIFTOOL_ARGS
|
||||||
|
|
||||||
if ebri.preferred_tag_reader == "exiv2" then
|
if ebri.preferred_tag_reader == "exiv2" then
|
||||||
@@ -270,7 +295,7 @@ local function is_burst_roll_image(image)
|
|||||||
end
|
end
|
||||||
line = tonumber(line)
|
line = tonumber(line)
|
||||||
if type(line) == "number" then
|
if type(line) == "number" then
|
||||||
if tonumber(line) > 0 then
|
if line > 0 then
|
||||||
log.msg(log.info, image.filename .. " is a burst roll container image")
|
log.msg(log.info, image.filename .. " is a burst roll container image")
|
||||||
result = true
|
result = true
|
||||||
end
|
end
|
||||||
@@ -278,6 +303,7 @@ local function is_burst_roll_image(image)
|
|||||||
end
|
end
|
||||||
p:close()
|
p:close()
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
restore_log_level(old_log_level)
|
restore_log_level(old_log_level)
|
||||||
return result
|
return result
|
||||||
@@ -296,7 +322,7 @@ local function group_extracted_dngs(image)
|
|||||||
local p = io.popen(cmd)
|
local p = io.popen(cmd)
|
||||||
if p then
|
if p then
|
||||||
for line in p:lines() do
|
for line in p:lines() do
|
||||||
if line:length() > 5 then -- skip blank lines created by forfiles
|
if line:len() > 5 then -- skip blank lines created by forfiles
|
||||||
local dng_image = dt.database.import(line)
|
local dng_image = dt.database.import(line)
|
||||||
dng_image:group_with(image)
|
dng_image:group_with(image)
|
||||||
count = count + 1
|
count = count + 1
|
||||||
@@ -321,6 +347,14 @@ local function extract_burst(image)
|
|||||||
restore_log_level(old_log_level)
|
restore_log_level(old_log_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function extract_burst_images(images)
|
||||||
|
for _, image in ipairs(images) do
|
||||||
|
if is_burst_roll_image(image) then
|
||||||
|
extract_burst(image)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function process_image(image)
|
local function process_image(image)
|
||||||
local old_log_level = set_log_level()
|
local old_log_level = set_log_level()
|
||||||
if is_burst_roll_image(image) then
|
if is_burst_roll_image(image) then
|
||||||
@@ -384,6 +418,37 @@ local function process_import(images)
|
|||||||
restore_log_level(old_log_level)
|
restore_log_level(old_log_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function select_burst_roll_images(event, images)
|
||||||
|
local old_log_level = set_log_level()
|
||||||
|
local selected = {}
|
||||||
|
|
||||||
|
for _, image in ipairs(images) do
|
||||||
|
if is_burst_roll_image(image) then
|
||||||
|
table.insert(selected, image)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
restore_log_level(old_log_level)
|
||||||
|
return selected
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_select_sensitive()
|
||||||
|
if #dt.collection > 0 then
|
||||||
|
dt.gui.libs.select.set_sensitive(MODULE, true)
|
||||||
|
else
|
||||||
|
dt.gui.libs.select.set_sensitive(MODULE, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_action_sensitive()
|
||||||
|
local sensitive = false
|
||||||
|
if #dt.gui.action_images > 0 then
|
||||||
|
if is_burst_roll_image(dt.gui.action_images[1]) then
|
||||||
|
sensitive = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
dt.gui.libs.image.set_sensitive(MODULE, sensitive)
|
||||||
|
end
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- M A I N P R O G R A M
|
-- M A I N P R O G R A M
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
@@ -394,6 +459,9 @@ if not DNGLAB then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if HAS_INTERNAL_METADATA then
|
||||||
|
log.msg(log.info, "Exif.Canon.RawBurstModeRoll detected, using internal metadata")
|
||||||
|
else
|
||||||
if not EXIFTOOL and not EXIV2 then
|
if not EXIFTOOL and not EXIV2 then
|
||||||
log.msg(log.error, MODULE .. _("no exif tag reader available"))
|
log.msg(log.error, MODULE .. _("no exif tag reader available"))
|
||||||
log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting..."))
|
log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting..."))
|
||||||
@@ -407,13 +475,29 @@ if not string.match(EXIFTOOL, ebri.preferred_tag_reader) and not string.match(EX
|
|||||||
log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader))
|
log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ebri.on_import = dt.preferences.read(MODULE, "on_import", "bool")
|
ebri.on_import = pref_read("on_import", "bool")
|
||||||
|
ebri.select_button = pref_read("select_button", "bool")
|
||||||
|
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- U S E R I N T E R F A C E
|
-- U S E R I N T E R F A C E
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
if ebri.select_button then
|
||||||
|
dt.gui.libs.select.register_selection(MODULE, _("select burst roll images"), select_burst_roll_images,
|
||||||
|
_("select burst roll image containers"))
|
||||||
|
end
|
||||||
|
|
||||||
|
dt.gui.libs.image.register_action(
|
||||||
|
MODULE,
|
||||||
|
_("extract burst roll images"),
|
||||||
|
function(event, images)
|
||||||
|
extract_burst_images(images)
|
||||||
|
end,
|
||||||
|
_("extract and group burst roll images with container")
|
||||||
|
)
|
||||||
|
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- D A R K T A B L E I N T E G R A T I O N
|
-- D A R K T A B L E I N T E G R A T I O N
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
@@ -421,6 +505,10 @@ ebri.on_import = dt.preferences.read(MODULE, "on_import", "bool")
|
|||||||
local function destroy()
|
local function destroy()
|
||||||
dt.destroy_event(MODULE, "post-import-film")
|
dt.destroy_event(MODULE, "post-import-film")
|
||||||
dt.destroy_event(MODULE, "post-import-image")
|
dt.destroy_event(MODULE, "post-import-image")
|
||||||
|
if ebri.select_button then
|
||||||
|
dt.gui.libs.select.destroy_selection(MODULE)
|
||||||
|
end
|
||||||
|
dt.gui.libs.image.action.destroy_action(MODULE)
|
||||||
end
|
end
|
||||||
|
|
||||||
script_data.destroy = destroy
|
script_data.destroy = destroy
|
||||||
@@ -444,6 +532,27 @@ dt.register_event(MODULE, "post-import-image",
|
|||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dt.register_event(MODULE, "selection-changed",
|
||||||
|
function(event)
|
||||||
|
set_action_sensitive()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if dt.preferences.read("darktable", "plugins/lighttable/act_on", "bool") then
|
||||||
|
dt.print_log("adding mouse-over-image-changed event")
|
||||||
|
dt.register_event(MODULE, "mouse-over-image-changed",
|
||||||
|
function(event, image)
|
||||||
|
set_action_sensitive()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
dt.register_event(MODULE, "collection-changed",
|
||||||
|
function(event)
|
||||||
|
set_select_sensitive()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
if not dt.query_event(MODULE, "shortcut") then
|
if not dt.query_event(MODULE, "shortcut") then
|
||||||
dt.register_event(MODULE, "shortcut",
|
dt.register_event(MODULE, "shortcut",
|
||||||
function(event, shortcut)
|
function(event, shortcut)
|
||||||
|
|||||||
@@ -117,10 +117,10 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = "group_persistence", -- name of script
|
name = _("group persistence"), -- name of script
|
||||||
purpose = _("preserve groups between darktable instances"), -- purpose of script
|
purpose = _("preserve groups between darktable instances"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/group_persistence" -- URL to help/documentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = _("regenerate_thumbnails"), -- visible name of script
|
name = _("regenerate thumbnails"), -- visible name of script
|
||||||
purpose = _("regenerate mipmap cache for selected images"), -- purpose of script
|
purpose = _("regenerate mipmap cache for selected images"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "https://docs.darktable.org/lua/development/lua.scripts.manual/scripts/official/regenerate_thumbnails" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/regenerate_thumbnails" -- URL to help/documentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ local function generate_thumbnails(images)
|
|||||||
local has_job = false
|
local has_job = false
|
||||||
|
|
||||||
if #images > 50 then
|
if #images > 50 then
|
||||||
rt.job = dt.gui.create_job("regenerating thumbnails", true, stop_job)
|
rt.job = dt.gui.create_job(_("regenerating thumbnails"), true, stop_job)
|
||||||
has_job = true
|
has_job = true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ dt.register_event(MODULE, "shortcut",
|
|||||||
if images then
|
if images then
|
||||||
generate_thumbnails(images)
|
generate_thumbnails(images)
|
||||||
end
|
end
|
||||||
end, "regenerate thumbnails"
|
end, _("regenerate thumbnails")
|
||||||
)
|
)
|
||||||
|
|
||||||
dt.register_event(MODULE, "selection-changed",
|
dt.register_event(MODULE, "selection-changed",
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = "scheduler", -- name of script
|
name = _("scheduler"), -- name of script
|
||||||
purpose = _("provide scheduler services for multitasking"), -- purpose of script
|
purpose = _("provide scheduler services for multitasking"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/scheduler/" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/scheduler/" -- URL to help/documentation
|
||||||
@@ -126,7 +126,8 @@ scheduler.log_level = log.debug
|
|||||||
-- P R E F E R E N C E S
|
-- P R E F E R E N C E S
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
dt.preferences.register(MODULE, "target_time_slice_seconds", "float", "scheduler preferred time slice", "target time slice before interrupt", 1.0, 0.1, 1.0, 0.1)
|
dt.preferences.register(MODULE, "target_time_slice_seconds", "float",
|
||||||
|
_("scheduler preferred time slice"), _("target time slice before interrupt"), 1.0, 0.1, 1.0, 0.1)
|
||||||
|
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- A L I A S E S
|
-- A L I A S E S
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = "select_raw_non_raw", -- name of script
|
name = _("select raw/non-raw"), -- name of script
|
||||||
purpose = _("select raw or non raw files from collection"), -- purpose of script
|
purpose = _("select raw or non raw files from collection"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_raw_non_raw" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_raw_non_raw" -- URL to help/documentation
|
||||||
@@ -158,8 +158,8 @@ end
|
|||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
dt.gui.libs.select.register_selection(MODULE .. "_raws", _("select raw"), select_raw_files,
|
dt.gui.libs.select.register_selection(MODULE .. "_raws", _("select raw"), select_raw_files,
|
||||||
_("select raw files"))
|
_("select raw files"))
|
||||||
dt.gui.libs.select.register_selection(MODULE .. "_non_raws", _("select non raw"), select_non_raw_files,
|
dt.gui.libs.select.register_selection(MODULE .. "_non_raws", _("select non-raw"), select_non_raw_files,
|
||||||
_("select non raw files"))
|
_("select non-raw files"))
|
||||||
|
|
||||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
-- D A R K T A B L E I N T E G R A T I O N
|
-- D A R K T A B L E I N T E G R A T I O N
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = _("select_unaltered"), -- visible name of script
|
name = _("select unaltered"), -- visible name of script
|
||||||
purpose = _("select images that have not been altered in any way"), -- purpose of script
|
purpose = _("select images that have not been altered in any way"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_unaltered/" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_unaltered/" -- URL to help/documentation
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
|||||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = _("selection2collection"), -- visible name of script
|
name = _("selection to collection"), -- visible name of script
|
||||||
purpose = _("create a temporary collection from a selection"), -- purpose of script
|
purpose = _("create a temporary collection from a selection"), -- purpose of script
|
||||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection2collection/" -- URL to help/documentation
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection2collection/" -- URL to help/documentation
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ local script_data = {}
|
|||||||
|
|
||||||
script_data.metadata = {
|
script_data.metadata = {
|
||||||
name = _("selection to PDF"),
|
name = _("selection to PDF"),
|
||||||
purpose = _("generate a pdf file of selected images"),
|
purpose = _("generate a PDF file of selected images"),
|
||||||
author = "Jérémy Rosen & Pascal Obry",
|
author = "Jérémy Rosen & Pascal Obry",
|
||||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection_to_pdf"
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection_to_pdf"
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ script_data.show = nil -- only required for libs since the destroy_method only h
|
|||||||
|
|
||||||
dt.preferences.register
|
dt.preferences.register
|
||||||
("selection_to_pdf","Open with","string",
|
("selection_to_pdf","Open with","string",
|
||||||
_("a pdf viewer"),
|
_("a PDF viewer"),
|
||||||
_("can be an absolute pathname or the tool may be in the PATH\nif not specified the system default will be used to open the pdf"),
|
_("can be an absolute pathname or the tool may be in the PATH\nif not specified the system default will be used to open the PDF"),
|
||||||
"")
|
"")
|
||||||
|
|
||||||
local title_widget = dt.new_widget("entry") {
|
local title_widget = dt.new_widget("entry") {
|
||||||
@@ -134,7 +134,7 @@ local function destroy()
|
|||||||
dt.print_log("done destroying")
|
dt.print_log("done destroying")
|
||||||
end
|
end
|
||||||
|
|
||||||
dt.register_storage("export_pdf", _("export thumbnails to pdf"),
|
dt.register_storage("export_pdf", _("export thumbnails to PDF"),
|
||||||
nil,
|
nil,
|
||||||
function(storage,image_table)
|
function(storage,image_table)
|
||||||
local my_title = title_widget.text
|
local my_title = title_widget.text
|
||||||
@@ -200,7 +200,7 @@ dt.register_storage("export_pdf", _("export thumbnails to pdf"),
|
|||||||
command = command .. " " .. pdffile
|
command = command .. " " .. pdffile
|
||||||
local result = dt.control.execute(command)
|
local result = dt.control.execute(command)
|
||||||
if result ~= 0 then
|
if result ~= 0 then
|
||||||
dt.print(_("problem running pdf viewer")) -- this one is probably usefull to the user
|
dt.print(_("problem running PDF viewer")) -- this one is probably usefull to the user
|
||||||
error("Problem running "..command)
|
error("Problem running "..command)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ local scripts_path = info.source:sub(2)
|
|||||||
-- assume user based
|
-- assume user based
|
||||||
local system_based = false
|
local system_based = false
|
||||||
|
|
||||||
if string.match(scripts_path, dt.configuration.data_dir) then
|
if string.match(scripts_path, ds.sanitize_lua(dt.configuration.data_dir)) then
|
||||||
log.msg(log.info, "script_manager using system based scripts")
|
log.msg(log.info, "script_manager using system based scripts")
|
||||||
system_based = true
|
system_based = true
|
||||||
end
|
end
|
||||||
@@ -748,7 +748,7 @@ local function scan_scripts(script_dir)
|
|||||||
local find_cmd = "find -L " .. script_dir .. " -name \\*.lua -print | sort"
|
local find_cmd = "find -L " .. script_dir .. " -name \\*.lua -print | sort"
|
||||||
|
|
||||||
if dt.configuration.running_os == "windows" then
|
if dt.configuration.running_os == "windows" then
|
||||||
find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" | sort"
|
find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" 2>nul | sort"
|
||||||
end
|
end
|
||||||
|
|
||||||
log.msg(log.debug, "find command is " .. find_cmd)
|
log.msg(log.debug, "find command is " .. find_cmd)
|
||||||
@@ -842,7 +842,7 @@ local function scan_repositories()
|
|||||||
local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.git -print | sort"
|
local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.git -print | sort"
|
||||||
|
|
||||||
if dt.configuration.running_os == "windows" then
|
if dt.configuration.running_os == "windows" then
|
||||||
find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git | sort"
|
find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git 2>nul | sort"
|
||||||
end
|
end
|
||||||
|
|
||||||
log.msg(log.debug, "find command is " .. find_cmd)
|
log.msg(log.debug, "find command is " .. find_cmd)
|
||||||
|
|||||||
Reference in New Issue
Block a user