Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions official/use_paired_jpg_as_mipmap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
--[[

use_paired_jpg_as_mipmap.lua - Use the JPG from the RAW JPG pair as the full size mipmap

Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.

This program 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.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
use_paired_jpg_as_mipmap - Use the JPG from the RAW JPG pair as the full size mipmap

use_paired_jpg_as_mipmap looks for RAW+JPEG image pairs as images are imported. After
import the JPEG image is copied to the mipmap cache as the full resolution mipmap. Requests
for smaller mipmap sizes can be satisfied by down sampling the full resolution mipmap. User's can
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for smaller mipmap sizes can be satisfied by down sampling the full resolution mipmap. User's can
for smaller mipmap sizes can be satisfied by down sampling the full resolution mipmap. Users can

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to use apostrophes way too often... :-(

decide whether or not to keep the paired JPEG files. The default is to keep them. If the
user doesn't want them, they are deleted after being copied to the mipmap cache.

ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT

None

USAGE

- Add the script to the lua-scripts
- Enable the script
- Turn off the keep_jpgs preference if the JPEGS are not wanted

BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>

CHANGES
]]

local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
-- local ds = require "lib/dtutils.string"
-- local dtsys = require "lib/dtutils.system"
local log = require "lib/dtutils.log"
-- local debug = require "darktable.debug"
Comment on lines +48 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the commented out lines be removed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably. When I write a script I use a boilerplate and a bash script that fills in some of the blanks. I tend to leave the above lines commented out because too often I found myself adding something and needing another library. But, when I'm done I'm sometimes lazy and don't clean up after myself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there are some other functions defined that aren't used. I read through all of them when learning how the script worked, only to find that some of them were unused, so it would be helpful to trim to just what is needed.



-- - - - - - - - - - - - - - - - - - - - - - - -
-- A P I C H E C K
-- - - - - - - - - - - - - - - - - - - - - - - -

du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need


-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -

local gettext = dt.gettext.gettext

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


-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- 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.destroy = nil -- function to destory the script
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
script_data.show = nil -- only required for libs since the destroy_method only hides them

script_data.metadata = {
name = _("use paired jpg as mipmap"), -- visible name of script
purpose = _("Use the JPG from the RAW JPG pair as the full size mipmap"), -- purpose of script
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/use_paired_jpg_as_mipmap/" -- URL to help/documentation
}


-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -

local MODULE <const> = "use_paired_jpg_as_mipmap"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir

-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"

-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"

-- max mipmap size - 5.4.x = 8, after is 10
local MAX_MIPMAP_SIZE <const> = (dt.configuration.version > "5.4.1") and "10" or "8"

-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -

log.log_level(DEFAULT_LOG_LEVEL)

-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -

local use_paired_jpg_as_mipmap = {}

-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -

use_paired_jpg_as_mipmap.log_level = DEFAULT_LOG_LEVEL
use_paired_jpg_as_mipmap.imported_images = {}
use_paired_jpg_as_mipmap.image_count = 0
use_paired_jpg_as_mipmap.mipmap_dir = nil
use_paired_jpg_as_mipmap.keep_jpgs = true

-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -

dt.preferences.register(MODULE, "keep_jpgs", "bool", script_data.metadata.name .. " - " .. _("keep JPEG files"),
_("don't delete JPEGs after copying them to mipmap folder"), true)

-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -

local namespace = use_paired_jpg_as_mipmap
local pmj = use_paired_jpg_as_mipmap

-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -

-------------------
-- helper functions
-------------------

local function set_log_level(level)
if not level then
level = namespace.log_level
end

local old_log_level = log.log_level()
log.log_level(level)

return old_log_level
end

local function restore_log_level(level)
if not level then
level = namespace.log_level
end
log.log_level(level)
end

local function reset_log_level()
log.log_level(DEFAULT_LOG_LEVEL)
namespace.log_level = DEFAULT_LOG_LEVEL
end

local function pref_read(name, pref_type)
local old_log_level = set_log_level(namespace.log_level)

log.msg(log.debug, MODULE .. ": name is " .. name .. " and type is " .. pref_type)

local val = dt.preferences.read(MODULE, name, pref_type)

log.msg(log.debug, MODULE .. ": read value " .. tostring(val))

restore_log_level(old_log_level)
return val
end

local function pref_write(name, pref_type, value)
local old_log_level = set_log_level(namespace.log_level)

log.msg(log.debug, MODULE .. ": writing value " .. tostring(value) .. " for name " .. name)

dt.preferences.write(MODULE, name, pref_type, value)

restore_log_level(old_log_level)
end

local function refresh_collection()
local rules = dt.gui.libs.collect.filter()
dt.gui.libs.collect.filter(rules)
end

local function get_mipmap_dir()
local old_log_level = set_log_level(pmj.log_level)
local mipmap_dir = nil
local cachedir = dt.configuration.cache_dir

local cmd = "cd " .. cachedir .. "; ls -d mip*"

if dt.configuration.running_os == "windows" then
cmd = "forfiles /P \"" .. cachedir .. "\" /M mip* /C \"cmd /c echo @file\""
end

local p = io.popen(cmd)
if p then
for line in p:lines() do
if line:len() > 4 then
mipmap_dir = cachedir .. PS .. line
log.msg(log.info, MODULE .. ": mipmap_dir is " .. mipmap_dir)
end
end
p:close()
end

restore_log_level(old_log_level)
return mipmap_dir
end

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

local function process_images(images, count)
local old_log_level = set_log_level(pmj.log_level)

-- if dt.collection is equal to count there are no
-- RAW+JPEG pairs

if count == #dt.collection then
log.msg(log.screen, _("No RAW+JPEG pairs to process"))
return
end

local job = dt.gui.create_job(_("Generating cache from jpgs"), true, stop_job)
local processed_count = 0
local mipmap_dir = get_mipmap_dir() .. PS .. MAX_MIPMAP_SIZE .. PS

local rc = df.mkdir(mipmap_dir)
log.msg(log.debug, MODULE .. ": rc from mkdir is " .. rc)

for k,v in pairs(images) do
if job.valid then
if v["raw"] and v["jpg"] then
local raw_image_id = v["raw"].id
local fname = v["jpg"].path .. PS .. v["jpg"].filename
df.file_copy(fname, mipmap_dir .. raw_image_id .. ".jpg")
if not pmj.keep_jpgs then
log.msg(log.debug, MODULE .. ": removing jpg")
v["jpg"]:delete()
local success, msg = os.remove(v["jpg"].path .. PS .. v["jpg"].filename)
if not success then
log.msg(log.warn, MODULE .. ": unable to remove jpg - reason: " .. msg)
end
success, msg = os.remove(v["jpg"].path .. PS .. v["jpg"].filename .. ".xmp")
if not success then
log.msg(log.warn, MODULE .. ": unable to remove jpg xmp - reason: " .. msg)
end
end
-- v["raw"]:generate_cache(true, 6, 6)
processed_count = processed_count + 1
end
job.percent = processed_count / count
end
dt.control.sleep(5)
end
stop_job(job)
if not pmj.keep_jpgs then
refresh_collection()
end
dt.util.message(MODULE, "broadcast", "finished")
end

-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -

use_paired_jpg_as_mipmap.keep_jpgs = pref_read("keep_jpgs", "bool")

-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -

-- - - - - - - - - - - - - - - - - - - - - - - -
-- D A R K T A B L E I N T E G R A T I O N
-- - - - - - - - - - - - - - - - - - - - - - - -

local function destroy()
dt.destroy_event(MODULE, "post-import-film")
dt.destroy_event(MODULE, "post-import-image")
end

script_data.destroy = destroy

-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -

dt.register_event(MODULE, "post-import-film",
function(event, film)
process_images(pmj.imported_images, pmj.image_count)
pmj.imported_images = {}
pmj.image_count = 0
end
)

dt.register_event(MODULE, "post-import-image",
function(event, image)
local basename = df.get_basename(image.filename)
local extension = string.lower(df.get_filetype(image.filename))

if not string.match(extension, "jpg") then
extension = "raw"
end

if not pmj.imported_images[basename] then
pmj.imported_images[basename] = {}
end

pmj.imported_images[basename][extension] = image

if not string.match(extension, "jpg") then
pmj.image_count = pmj.image_count + 1
end
end
)

return script_data