darktable-org / lua-scripts

159 stars 112 forks source link

Allow programatically reading / setting output color profile of an image to be exported #522

Open koto opened 2 months ago

koto commented 2 months ago

Forked from pixls.us thread:

In a script, I'm using the following code to export an image to JPG:

local exporter = dt.new_format("jpeg")
exporter.quality = 95
exporter:write_image(src, dest)

That code is dependent on the export module's output color profile currently selected in the UI. I would like to either force a specific profile (let's say Display P3 RGB), or detect when an invalid profile (for my purpose) is used, to be able to warn the user. My options with the current API are:

  1. dt.gui.action("lib/export/profile", 0, "selection", "first", 0.0) + dt.gui.action("lib/export/profile", 0, "selection", "next", 10.0) as Display P3 RGB is currently the 11th one displayed; that's hardcoded in colorspaces.c.

    Pro: Unobtrusive. Con: Depends on DT internals without their proper representation through the API. If DT profile order ever changes, the script forcefully selects a different profile.

  2. dt.gui.action("lib/export/profile", 0, "selection", "popup", 0.0)

    Pro: Doesn't depend on DT internals, user could select their own ICC profiles too. Con: Obtrusive; user is prompted to the action even if the current settings are correct, they are also asked to select the only correct option.

  3. Export, then check the resulting file metadata, and error out post factum. Pro: Unobtrusive. Con: Suboptimal UX, as I cannot prevent the error from happening, or implement input data validation before triggering the action. Possibly the metadata won't be there if export doesn't have the EXIF.
  4. Export outside of DT Pro: Total control over the output. Con: Needs an intermediary export in DT to a format that isn't sensitive to color profile (does it even exist?) to retain changes done to the RAW in DT, like cropping etc.

All of the workarounds above would not be needed if the Lua API allowed for:

koto commented 2 months ago

The current export output color profile is also stored in a DT preference (plugins/lighttable/export/icctype=26 for Display P3 RGB), but I can't read core DT preferences from Lua (only enumerate them with dartktable.preferences.get_keys).

Edit: scratch that,

dt.preferences.read("darktable", "plugins/lighttable/export/icctype", "integer") -- 26

seems to work.

wpferguson commented 2 months ago

Reading code I find...

 // TODO: expose these to the user!
  gboolean high_quality = dt_conf_get_bool("plugins/lighttable/export/high_quality_processing");
  gboolean export_masks = dt_conf_get_bool("plugins/lighttable/export/export_masks");
  // TODO: expose icc overwrites to the user!
  dt_colorspaces_color_profile_type_t icc_type = dt_conf_get_int("plugins/lighttable/export/icctype");
  const char *icc_filename = dt_conf_get_string_const("plugins/lighttable/export/iccprofile");

Reading/setting output color profile of dt_lua_image_t

Not part of the image. The image record has a colorspace entry, but is SRGB or Adobe RGB as used by the camera.

Reading/setting the combobox options by their name.

They are translated, so the comparison code would have to be translated too. We are working on getting the Lua scripts to use darktable translation, so this may be possible in the future. It would probably require changes to dt.gui.action() to expose the information.

For the options above...

Which leaves option 1 (at least for now)...

koto commented 2 months ago

Which leaves option 1 (at least for now)...

That's what I did in the end in https://github.com/darktable-org/lua-scripts/pull/502/commits/6a835ce1a271439d2971756eb6fb95e478ebab67, with a slightly improved method. I go to first option, and then trigger next + read off config value, until I get the hardcoded 26; so the code only depends on a few const values that are very unlikely to change, and I don't have to rely on the ordering in the combobox. I only have to sleep 10ms after each step, so that's quite all right for my purpose.

koto commented 1 week ago

FWIW this workaround still has occasional race conditions, I noticed I had to increase the sleep time when switching to faster device recently.

wpferguson commented 1 week ago

Here's a piece of code I wrote today to see what I could do with dt.gui.action(). My last thought was to try just setting it directly and I figured out a way to do it. Feel free to use what you want...

local dt = require "darktable"

local MODULE = "export_profiles"

local export_profiles = {
  "image settings",         -- -1 in dt.gui.action
  "sRGB(web-safe)",         -- -2       ""
  "Adobe RGB(compatible)",  -- -3       ""
  "linear Rec709 RGGB",     -- -4       ""
  "Rec709 RGB",             -- -5       ""
  "linear Rec2020 RGB",     -- -6       ""
  "PQ Rec2020 RGB",         -- -7       ""
  "HLG Rec2020 RGB",        -- -8       ""
  "PQ P3 RGB",              -- -9       ""
  "HLG P3 RGB",             -- -10      ""
  "Display P3 RGB",         -- -11      ""
  "linear ProPhoto RGB",    -- -12      ""
  "BRG (for testing)"       -- -13      ""
}

local function get_profile_index(name)
  for i,v in ipairs(export_profiles) do
    if name == v then
      return i
    end
  end
  dt.print_error("profile " .. name .. " not found")
  return nil
end

dt.register_event(MODULE .. "_get_current", "shortcut",
  function(event, shortcut)
    dt.print("current profile is " .. export_profiles[dt.gui.action("lib/export/profile", 0, "selection", "", "") * -1])
  end,
  "get current export profile"
)

dt.register_event(MODULE .. "_set_display_rgb", "shortcut",
  function(event, shortcut)
    dt.gui.action("lib/export/profile", 0, "selection", "next", get_profile_index("Display P3 RGB") - dt.gui.action("lib/export/profile", 0, "selection", "", "") * -1)
  end,
  "set export profile to Display P3 RGB"
)

dt.register_event(MODULE .. "_set_display_rgb_direct", "shortcut",
  function(event, shortcut)
    dt.gui.action("lib/export/profile", 0, "selection", "", -11)
  end,
  "set export profile to Display P3 RGB directly"
)
koto commented 6 days ago

Thanks, I like using the offset trick! So it seems like we either have the race condition or hardcoding the profile display order (and TBH the latter seems preferred now). I could also just make it a config setting to fallback to old method in case things change in the future.

dt.gui.action("lib/export/profile", 0, "selection", "", -11) shows a popup for me in 4.8.1 instead of setting the value, is this different in DT from HEAD?

wpferguson commented 6 days ago

Tested in 4.8.1 and I got a popup too. Not sure what changed in master, but it works there.

The offset trick works well and should work with earlier versions, so it's probably the safer choice. The direct was just a guess on my part to see what would happen, so it's undocumented at best and a fluke at worst.