Skip to main content
Participating Frequently
November 1, 2015
Answered

Copy develop settings from one photo to another: having problems! photo:getDevelopSettings LrDevelopController.setValue

  • November 1, 2015
  • 3 replies
  • 2642 views

I've also attached the full code at the bottom of this message in case that helps, but, to summarize,

I have a lua plugin (MIDI2LR by rsjaffe) that receives MIDI messages from an application through a LrSocket. Everything has been working, but I've tried to add copy/paste develop settings without success.

In the following code, the copy settings is:

function copySettings()

    local photo = LrApplication.activeCatalog():getTargetPhoto()

    settings = photo:getDevelopSettings()

end

and the paste settings is:

function pasteSettings()

    applySettings(settings) 

end


function applySettings(set) --still experimental

    if LrApplicationView.getCurrentModuleName() ~= 'develop' then

            LrApplicationView.switchToModule('develop')

    end

    for x,v in pairs(set) do

--      SERVER:send(string.format('%s %d\n', x, develop_lerp_to_midi(v)))

--      PARAM_OBSERVER = v

      LrDevelopController.setValue(x,v)

    end

end


The functions do get called, but nothing happens to the target photo. I've also been unable to attach a debugger (tried ZeroBrane studio as described in Debugging Adobe Lightroom plugins with ZeroBrane Studio - ZeroBrane but can't get Lightroom to load mobdebug.lrmodule.


Can someone point out where I'm going wrong?

Thanks.


require 'strict.lua' -- catch some incorrect variable names

require 'Develop_Params.lua' -- global table of develop params we need to observe

local LrApplication      = import 'LrApplication'

local LrApplicationView  = import 'LrApplicationView'

local LrDevelopController = import 'LrDevelopController'

local LrFunctionContext  = import 'LrFunctionContext'

local LrSelection        = import 'LrSelection'

local LrShell            = import 'LrShell'

local LrSocket            = import 'LrSocket'

local LrTasks            = import 'LrTasks'

local LrUndo              = import 'LrUndo'

-- File-local consts

local RECEIVE_PORT = 58763

local SEND_PORT    = 58764

local PICKUP_THRESHOLD = 4

-- File-local vars

local CopyUUID

local settings

local LAST_PARAM = ''

local PARAM_OBSERVER = {}

local PICKUP_ENABLED = true

local SERVER = {}

--File-local function declarations, advance declared to allow it to be in scope for all calls.

--When defining pre-declared function, DO NOT USE local KEYWORD again, as it will define yet another local function.

--These declaration are intended to get around some Lua gotcha's.

local applySettings

local copySettings

local develop_lerp_to_midi

local midi_lerp_to_develop

local pasteSettings

local processMessage

local sendChangedParams

local startServer

local updateParam

local ACTIONS = {

    ['DecrementLastDevelopParameter'] = function () LrDevelopController.decrement(LAST_PARAM) end,

    ['VirtualCopy']      = function () LrApplication.activeCatalog():createVirtualCopies() end,

    ['ToggleScreenTwo']  = LrApplicationView.toggleSecondaryDisplay,

    ['CopySettings']    = copySettings,

    ['PasteSettings']    = pasteSettings,

}

local TOOL_ALIASES = {

    ['Loupe']          = 'loupe',

    ['CropOverlay']    = 'crop',

    ['SpotRemoval']    = 'dust',

    ['RedEye']          = 'redeye',

    ['GraduatedFilter'] = 'gradient',

    ['RadialFilter']    = 'circularGradient',

    ['AdjustmentBrush'] = 'localized',

}

local SETTINGS = {

    ['Pickup'] = function(enabled) PICKUP_ENABLED = (enabled == 1) end,

}

function copySettings()

    local photo = LrApplication.activeCatalog():getTargetPhoto()

    settings = photo:getDevelopSettings()

end

function pasteSettings()

    applySettings(settings) 

end

function midi_lerp_to_develop(param, midi_value)

    -- map midi range to develop parameter range

    local min,max = LrDevelopController.getRange(param)

--    if(param == 'Temperature') then

--        min = 3000

--        max = 9000

--    end

   

    local result = midi_value/127 * (max-min) + min

    return result

end

function develop_lerp_to_midi(param)

    -- map develop parameter range to midi range

    local min, max = LrDevelopController.getRange(param)

--    if(param == 'Temperature') then

--        min = 3000

--        max = 9000

--    end

   

    local result = (LrDevelopController.getValue(param)-min)/(max-min) * 127

    return result

end

function updateParam(param, midi_value)

    -- this function does a 'pickup' type of check

    -- that is, it will ensure the develop parameter is close

    -- to what the inputted command value is before updating it

    if LrApplicationView.getCurrentModuleName() ~= 'develop' then

            LrApplicationView.switchToModule('develop')

    end

   

    if((not PICKUP_ENABLED) or (math.abs(midi_value - develop_lerp_to_midi(param)) <= PICKUP_THRESHOLD)) then

        PARAM_OBSERVER[param] = midi_lerp_to_develop(param, midi_value)

        LrDevelopController.setValue(param, midi_lerp_to_develop(param, midi_value))

        LAST_PARAM = param

    end

end

function applySettings(set) --still experimental

    if LrApplicationView.getCurrentModuleName() ~= 'develop' then

            LrApplicationView.switchToModule('develop')

    end

    for x,v in pairs(set) do

--      SERVER:send(string.format('%s %d\n', x, develop_lerp_to_midi(v)))

--      PARAM_OBSERVER = v

      LrDevelopController.setValue(x,v)

    end

end

-- message processor

function processMessage(message)

    if type(message) == 'string' then

        -- messages are in the format 'param value'

        local _, _, param, value = string.find( message, '(%S+)%s(%d+)' )

     

        if(ACTIONS[param] ~= nil) then -- perform a one time action

            if(tonumber(value) == 127) then ACTIONS[param]() end

        elseif(param:find('Reset') == 1) then -- perform a reset other than those explicitly coded in ACTIONS array

          if(tonumber(value) == 127) then LrDevelopController.resetToDefault(param:sub(6)) end

        elseif(param:find('SwToM') == 1) then -- perform a switch to module

            if(tonumber(value) == 127) then LrApplicationView.switchToModule(param:sub(6)) end

        elseif(param:find('ShoVw') == 1) then -- change application's view mode

            if(tonumber(value) == 127) then LrApplicationView.showView(param:sub(6)) end

        elseif(param:find('ShoScndVw') == 1) then -- change application's view mode

            if(tonumber(value) == 127) then LrApplicationView.showSecondaryView(param:sub(10)) end

        elseif(TOOL_ALIASES[param] ~= nil) then -- switch to desired tool

            if(tonumber(value) == 127) then

                if(LrDevelopController.getSelectedTool() == TOOL_ALIASES[param]) then -- toggle between the tool/loupe

                    LrDevelopController.selectTool('loupe')

                else

                    LrDevelopController.selectTool(TOOL_ALIASES[param])

                end

            end

        elseif(SETTINGS[param] ~= nil) then

            SETTINGS[param](tonumber(value))

        else -- otherwise update a develop parameter

            updateParam(param, tonumber(value))

        end

    end

end

-- send changed parameters to MIDI2LR

function sendChangedParams( observer )

    for _, param in ipairs(DEVELOP_PARAMS) do

        if(observer[param] ~= LrDevelopController.getValue(param)) then

            SERVER:send(string.format('%s %d\n', param, develop_lerp_to_midi(param)))

            observer[param] = LrDevelopController.getValue(param)

            LAST_PARAM = param

        end

    end

end

function startServer(context)

    SERVER = LrSocket.bind {

          functionContext = context,

          plugin = _PLUGIN,

          port = SEND_PORT,

          mode = 'send',

          onClosed = function( socket ) -- this callback never seems to get called...

            -- MIDI2LR closed connection, allow for reconnection

            -- socket:reconnect()

          end,

          onError = function( socket, err )

            socket:reconnect()

          end,

        }

end

-- Main task

LrTasks.startAsyncTask( function()

    LrFunctionContext.callWithContext( 'socket_remote', function( context )

        LrDevelopController.revealAdjustedControls( true ) -- reveal affected parameter in panel track

       

        -- add an observer for develop param changes

        LrDevelopController.addAdjustmentChangeObserver( context, PARAM_OBSERVER, sendChangedParams )

       

        local client = LrSocket.bind {

            functionContext = context,

            plugin = _PLUGIN,

            port = RECEIVE_PORT,

            mode = 'receive',

            onMessage = function(socket, message)

                processMessage(message)

            end,

            onClosed = function( socket )

                -- MIDI2LR closed connection, allow for reconnection

                socket:reconnect()

               

                -- calling SERVER:reconnect causes LR to hang for some reason...

                SERVER:close()

                startServer(context)

            end,

            onError = function(socket, err)

                if err == 'timeout' then -- reconnect if timed out

                    socket:reconnect()

                end

            end

        }

       

        startServer(context)

       

        while true do

            LrTasks.sleep( 1/2 )

        end

       

        client:close()

        SERVER:close()

    end )

end )

LrTasks.startAsyncTask( function()

    if(WIN_ENV) then

        LrShell.openFilesInApp({_PLUGIN.path..'/Info.lua'}, _PLUGIN.path..'/MIDI2LR.exe')

    else

        LrShell.openFilesInApp({_PLUGIN.path..'/Info.lua'}, _PLUGIN.path..'/MIDI2LR.app') -- On Mac it seems like the files argument has to include an existing file

    end

end)

This topic has been closed for replies.
Correct answer StefanKeller42

this may be a correct way, it seems to work

local settings

function copySettings()

  LrTasks.startAsyncTask ( function ()

    local photo = LrApplication.activeCatalog():getTargetPhoto()

    settings = photo:getDevelopSettings()

  end )

end

function pasteSettings()

  if settings ~= nil then

    LrTasks.startAsyncTask ( function ()

      catalog = LrApplication.activeCatalog()

      catalog:withWriteAccessDo( "pasteSettings", function()

        local photo = catalog:getTargetPhoto()

        photo:applyDevelopSettings(settings)

      end )

    end )

  end

end

3 replies

StefanKeller42Correct answer
Participating Frequently
November 21, 2015

this may be a correct way, it seems to work

local settings

function copySettings()

  LrTasks.startAsyncTask ( function ()

    local photo = LrApplication.activeCatalog():getTargetPhoto()

    settings = photo:getDevelopSettings()

  end )

end

function pasteSettings()

  if settings ~= nil then

    LrTasks.startAsyncTask ( function ()

      catalog = LrApplication.activeCatalog()

      catalog:withWriteAccessDo( "pasteSettings", function()

        local photo = catalog:getTargetPhoto()

        photo:applyDevelopSettings(settings)

      end )

    end )

  end

end

Participating Frequently
November 26, 2015

Thank you very much for your assistance. The final version of the code follows.

To find out what was wrong, I put in debug print statements -- that made it clear that problem #1 was Lightroom doesn't completely unload one version of a long-running plugin when you reload a new version! I was making edits and reloading the plugin in LR. However, the main plugin task thread runs continuously in my plugin, and LR doesn't terminate that thread when it reloads a plugin--so I'd either have to, in the main plugin task thread, monitor for plugin termination, or restart LR to test the changes.

local function PasteSettings  ()

  LrTasks.startAsyncTask ( function ()

      LrApplication.activeCatalog():withWriteAccessDo(

        'Paste settings',

        function() LrApplication.activeCatalog():getTargetPhoto():applyDevelopSettings(MIDI2LR.Copied_Settings) end,

        { timeout = 4,

          callback = function() LrDialogs.showError('Unable to get catalog write access for copy settings') end,

          asynchronous = true }

      )

    end )

end

local function CopySettings ()

  LrTasks.startAsyncTask (

    function () MIDI2LR.Copied_Settings = LrApplication.activeCatalog():getTargetPhoto():getDevelopSettings() end

  )

end

johnrellis
Legend
November 27, 2015

Lightroom doesn't completely unload one version of a long-running plugin when you reload a new version! ...so I'd either have to, in the main plugin task thread, monitor for plugin termination, or restart LR to test the changes.

I believe you can also disable and re-enable the plugin, but I'm not 100% sure about that.

The technique I use for handling reloads is pretty simple.  The .lua file defining the background task defines a global variable that gets incremented every time the file gets loaded:

currentLoadVersion = rawget (_G, "currentLoadVersion") or 0

    --[[ Incremented every time the module is loaded. Avoid triggering

    "undeclared variable" from strict.lua.]]

The use of "rawget" ensures that strict.lua won't think there's an access to an undeclared variable.  (You should definitely use strict.lua, if you're not.)

When it first starts, the background task increments "currentLoadVersion" and then periodically checks for changes to its value, indicating the file has been reloaded:

LrTasks.startAsyncTask (Debug.showErrors (function ()

    currentLoadVersion = currentLoadVersion + 1

    local loadVersion = currentLoadVersion

    while (loadVersion == currentLoadVersion) do

        ...do some work...

        LrTasks.sleep (...)

        end

Info.lua also lets you define scripts that get invoked when the plugin is reloaded or when LR itself is shutting down.  But they need to communicate with the background task using a similar mechanism, with a global counter that the background task periodically checks.

Participating Frequently
November 21, 2015

there are some mistakes:

1. getDevelopSettings has to be called in an AsyncTask

2. Values are numbers, booleans and strings.

   booleans can be changed, but I guess string settings (as ToneCurveName) are no way suported now

3.some Values with 2012 in the name are not working, at least Exposure

the SDK says:

WARNING:The develop settings APIs are considered experimental. You should not depend on the contents of the settings table remaining compatible in future versions of Lightroom. The definitive list is the one shown in the UI.

my source based on yours:

you may map the 2012 settings to the correct ones...

local settings

function copySettings()

  LrTasks.startAsyncTask ( function ()

    local photo = LrApplication.activeCatalog():getTargetPhoto()

    settings = photo:getDevelopSettings()

  end )

end

function pasteSettings()

   applySettings(settings)

end

function applySettings(set) -- not working eg with 2012 Settings

    for x,v in pairs(set) do

      if type (v) == "boolean"

        then

          if v then v=1 else v=0 end

        end

      if type (v) == "number"

        then

          --      SERVER:send(string.format('%s %d\n', x, develop_lerp_to_midi(v)))

          --      PARAM_OBSERVER = v

        --dbgout (string.format('%s is %d', x, v))

        LrDevelopController.setValue(x,v)

        end

      if type (v) == "string"

        then

        -- ??????

        end

    end

end

johnrellis
Legend
November 2, 2015

Re debugging: You absolutely need a more functional method of debugging than print statements.  See this thread for how someone else got ZeroBrane to work for them: Interactive debugger for Lightroom plugins.

If that doesn't work, you can use my free Debugging Toolkit for Lightroom SDK. It's not as capable as a full-fledged IDE, but it's much better than print statements.

johnrellis
Legend
November 2, 2015

Re your use of LrDevelopController: I haven't seen much experience posted here yet.  It's not clear from the documentation whether the client needs to call LrDevelopController.startTracking() / stopTracking() around a call to setValue().

There is also the undocumented photo:applyDevelopSettings().   However, if you are connecting to a MIDI controller for interactive use, LrDevelopController looks more appropriate, with regard to history states and control over interactive updates of the display.