@pired/lightroom-mcp 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/LightroomMCP.lrplugin/HandlerAI.lua +466 -0
- package/dist/LightroomMCP.lrplugin/HandlerAIMasks.lua +549 -0
- package/dist/LightroomMCP.lrplugin/HandlerCatalog.lua +256 -0
- package/dist/LightroomMCP.lrplugin/HandlerCollections.lua +337 -0
- package/dist/LightroomMCP.lrplugin/HandlerDevelop.lua +1668 -0
- package/dist/LightroomMCP.lrplugin/HandlerExport.lua +149 -0
- package/dist/LightroomMCP.lrplugin/HandlerImport.lua +108 -0
- package/dist/LightroomMCP.lrplugin/HandlerLocalAdjustments.lua +330 -0
- package/dist/LightroomMCP.lrplugin/HandlerMetadata.lua +169 -0
- package/dist/LightroomMCP.lrplugin/HandlerOrganization.lua +783 -0
- package/dist/LightroomMCP.lrplugin/HandlerPreview.lua +189 -0
- package/dist/LightroomMCP.lrplugin/HandlerSearch.lua +185 -0
- package/dist/LightroomMCP.lrplugin/HandlerSelection.lua +302 -0
- package/dist/LightroomMCP.lrplugin/HandlerSpots.lua +313 -0
- package/dist/LightroomMCP.lrplugin/HandlerWatermark.lua +59 -0
- package/dist/LightroomMCP.lrplugin/Info.lua +25 -0
- package/dist/LightroomMCP.lrplugin/JSON.lua +270 -0
- package/dist/LightroomMCP.lrplugin/Log.lua +92 -0
- package/dist/LightroomMCP.lrplugin/MenuShowStatus.lua +22 -0
- package/dist/LightroomMCP.lrplugin/PhotoLookup.lua +39 -0
- package/dist/LightroomMCP.lrplugin/PluginInfoProvider.lua +865 -0
- package/dist/LightroomMCP.lrplugin/PluginInit.lua +48 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +31 -0
- package/dist/cli.js.map +1 -0
- package/dist/configs/claude-code.mcp.json +9 -0
- package/dist/configs/claude-desktop.json +12 -0
- package/dist/configs/generic-stdio-client.json +10 -0
- package/dist/configs/mcp-proxy-http.json +9 -0
- package/dist/create-server.d.ts +12 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +17 -0
- package/dist/create-server.js.map +1 -0
- package/dist/deepseek-harness/README.md +112 -0
- package/dist/deepseek-harness/deepseek-mcp-harness.mjs +292 -0
- package/dist/dispatcher.d.ts +34 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +72 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/heartbeat.d.ts +17 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +36 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +255 -0
- package/dist/index.js.map +1 -0
- package/dist/install-plugin.d.ts +16 -0
- package/dist/install-plugin.d.ts.map +1 -0
- package/dist/install-plugin.js +125 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/instance-lock.d.ts +5 -0
- package/dist/instance-lock.d.ts.map +1 -0
- package/dist/instance-lock.js +77 -0
- package/dist/instance-lock.js.map +1 -0
- package/dist/list-tools-handler.d.ts +16 -0
- package/dist/list-tools-handler.d.ts.map +1 -0
- package/dist/list-tools-handler.js +50 -0
- package/dist/list-tools-handler.js.map +1 -0
- package/dist/plugin-liveness.d.ts +48 -0
- package/dist/plugin-liveness.d.ts.map +1 -0
- package/dist/plugin-liveness.js +75 -0
- package/dist/plugin-liveness.js.map +1 -0
- package/dist/plugin-socket.d.ts +32 -0
- package/dist/plugin-socket.d.ts.map +1 -0
- package/dist/plugin-socket.js +97 -0
- package/dist/plugin-socket.js.map +1 -0
- package/dist/ports.d.ts +3 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +22 -0
- package/dist/ports.js.map +1 -0
- package/dist/token.d.ts +3 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +23 -0
- package/dist/token.js.map +1 -0
- package/dist/tool-contracts.d.ts +13 -0
- package/dist/tool-contracts.d.ts.map +1 -0
- package/dist/tool-contracts.js +1422 -0
- package/dist/tool-contracts.js.map +1 -0
- package/dist/tool-handler.d.ts +23 -0
- package/dist/tool-handler.d.ts.map +1 -0
- package/dist/tool-handler.js +96 -0
- package/dist/tool-handler.js.map +1 -0
- package/dist/validate-args.d.ts +7 -0
- package/dist/validate-args.d.ts.map +1 -0
- package/dist/validate-args.js +37 -0
- package/dist/validate-args.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/wait-until.d.ts +7 -0
- package/dist/wait-until.d.ts.map +1 -0
- package/dist/wait-until.js +15 -0
- package/dist/wait-until.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1668 @@
|
|
|
1
|
+
local LrApplication = import 'LrApplication'
|
|
2
|
+
local LrFileUtils = import 'LrFileUtils'
|
|
3
|
+
local LrPathUtils = import 'LrPathUtils'
|
|
4
|
+
local LrTasks = import 'LrTasks'
|
|
5
|
+
local LrApplicationView = import 'LrApplicationView'
|
|
6
|
+
local LrDevelopController = import 'LrDevelopController'
|
|
7
|
+
|
|
8
|
+
local PhotoLookup = require 'PhotoLookup'
|
|
9
|
+
local Log = require 'Log'
|
|
10
|
+
|
|
11
|
+
local DevelopHandler = {}
|
|
12
|
+
|
|
13
|
+
local MAX_BULK_PHOTO_IDS = 1000
|
|
14
|
+
local MAX_POINT_CURVE_VALUES = 64
|
|
15
|
+
|
|
16
|
+
local POINT_CURVE_SETTING_KEYS = {
|
|
17
|
+
"ToneCurvePV2012",
|
|
18
|
+
"ToneCurvePV2012Red",
|
|
19
|
+
"ToneCurvePV2012Green",
|
|
20
|
+
"ToneCurvePV2012Blue",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
local ALLOWED_DEVELOP_SETTING_KEYS = {
|
|
24
|
+
"WhiteBalance",
|
|
25
|
+
"Temperature",
|
|
26
|
+
"Tint",
|
|
27
|
+
"Exposure2012",
|
|
28
|
+
"Contrast2012",
|
|
29
|
+
"Highlights2012",
|
|
30
|
+
"Shadows2012",
|
|
31
|
+
"Whites2012",
|
|
32
|
+
"Blacks2012",
|
|
33
|
+
"Texture",
|
|
34
|
+
"Clarity2012",
|
|
35
|
+
"Dehaze",
|
|
36
|
+
"Vibrance",
|
|
37
|
+
"Saturation",
|
|
38
|
+
"SaturationAdjustmentRed",
|
|
39
|
+
"SaturationAdjustmentOrange",
|
|
40
|
+
"SaturationAdjustmentYellow",
|
|
41
|
+
"SaturationAdjustmentGreen",
|
|
42
|
+
"SaturationAdjustmentAqua",
|
|
43
|
+
"SaturationAdjustmentBlue",
|
|
44
|
+
"SaturationAdjustmentPurple",
|
|
45
|
+
"SaturationAdjustmentMagenta",
|
|
46
|
+
"HueAdjustmentRed",
|
|
47
|
+
"HueAdjustmentOrange",
|
|
48
|
+
"HueAdjustmentYellow",
|
|
49
|
+
"HueAdjustmentGreen",
|
|
50
|
+
"HueAdjustmentAqua",
|
|
51
|
+
"HueAdjustmentBlue",
|
|
52
|
+
"HueAdjustmentPurple",
|
|
53
|
+
"HueAdjustmentMagenta",
|
|
54
|
+
"LuminanceAdjustmentRed",
|
|
55
|
+
"LuminanceAdjustmentOrange",
|
|
56
|
+
"LuminanceAdjustmentYellow",
|
|
57
|
+
"LuminanceAdjustmentGreen",
|
|
58
|
+
"LuminanceAdjustmentAqua",
|
|
59
|
+
"LuminanceAdjustmentBlue",
|
|
60
|
+
"LuminanceAdjustmentPurple",
|
|
61
|
+
"LuminanceAdjustmentMagenta",
|
|
62
|
+
"ParametricShadows",
|
|
63
|
+
"ParametricDarks",
|
|
64
|
+
"ParametricLights",
|
|
65
|
+
"ParametricHighlights",
|
|
66
|
+
"ParametricShadowSplit",
|
|
67
|
+
"ParametricMidtoneSplit",
|
|
68
|
+
"ParametricHighlightSplit",
|
|
69
|
+
"ToneCurvePV2012",
|
|
70
|
+
"ToneCurvePV2012Red",
|
|
71
|
+
"ToneCurvePV2012Green",
|
|
72
|
+
"ToneCurvePV2012Blue",
|
|
73
|
+
"ToneCurveName2012",
|
|
74
|
+
"ConvertToGrayscale",
|
|
75
|
+
"Sharpness",
|
|
76
|
+
"SharpenRadius",
|
|
77
|
+
"SharpenDetail",
|
|
78
|
+
"SharpenEdgeMasking",
|
|
79
|
+
"LuminanceSmoothing",
|
|
80
|
+
"LuminanceNoiseReductionDetail",
|
|
81
|
+
"LuminanceNoiseReductionContrast",
|
|
82
|
+
"ColorNoiseReduction",
|
|
83
|
+
"ColorNoiseReductionDetail",
|
|
84
|
+
"ColorNoiseReductionSmoothness",
|
|
85
|
+
"LensProfileEnable",
|
|
86
|
+
"LensManualDistortionAmount",
|
|
87
|
+
"PerspectiveVertical",
|
|
88
|
+
"PerspectiveHorizontal",
|
|
89
|
+
"PerspectiveRotate",
|
|
90
|
+
"PerspectiveScale",
|
|
91
|
+
"PerspectiveAspect",
|
|
92
|
+
"PerspectiveUpright",
|
|
93
|
+
"PostCropVignetteAmount",
|
|
94
|
+
"PostCropVignetteMidpoint",
|
|
95
|
+
"PostCropVignetteRoundness",
|
|
96
|
+
"PostCropVignetteFeather",
|
|
97
|
+
"PostCropVignetteStyle",
|
|
98
|
+
"GrainAmount",
|
|
99
|
+
"GrainSize",
|
|
100
|
+
"GrainFrequency",
|
|
101
|
+
"CropTop",
|
|
102
|
+
"CropLeft",
|
|
103
|
+
"CropBottom",
|
|
104
|
+
"CropRight",
|
|
105
|
+
"CropAngle",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
local ALLOWED_DEVELOP_SETTING_LOOKUP = {}
|
|
109
|
+
for _, key in ipairs(ALLOWED_DEVELOP_SETTING_KEYS) do
|
|
110
|
+
ALLOWED_DEVELOP_SETTING_LOOKUP[key] = true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
local POINT_CURVE_SETTING_LOOKUP = {}
|
|
114
|
+
for _, key in ipairs(POINT_CURVE_SETTING_KEYS) do
|
|
115
|
+
POINT_CURVE_SETTING_LOOKUP[key] = true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
local function requireString(value, name)
|
|
119
|
+
if type(value) ~= "string" or value == "" then
|
|
120
|
+
error(name .. " is required")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
-- Photo ids leave the catalog as numbers (localIdentifier), so a caller feeding
|
|
125
|
+
-- search/selection output back in sends numbers. PhotoLookup normalizes with
|
|
126
|
+
-- tostring; only these validators used to reject them, which surfaced as a
|
|
127
|
+
-- misleading "photo_id is required".
|
|
128
|
+
local function requirePhotoId(value, name)
|
|
129
|
+
if type(value) == "number" then
|
|
130
|
+
return tostring(value)
|
|
131
|
+
end
|
|
132
|
+
requireString(value, name)
|
|
133
|
+
return value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
local function requireStringArray(value, name, maxItems)
|
|
137
|
+
if type(value) ~= "table" then
|
|
138
|
+
error(name .. " is required")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
local count = 0
|
|
142
|
+
for key, item in pairs(value) do
|
|
143
|
+
if type(key) ~= "number" or key < 1 or key ~= math.floor(key) then
|
|
144
|
+
error(name .. " must be an array")
|
|
145
|
+
end
|
|
146
|
+
if type(item) ~= "string" or item == "" then
|
|
147
|
+
error(name .. "[" .. tostring(key) .. "] must be a non-empty string")
|
|
148
|
+
end
|
|
149
|
+
count = count + 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if count == 0 then
|
|
153
|
+
error(name .. " is required")
|
|
154
|
+
end
|
|
155
|
+
if count ~= #value then
|
|
156
|
+
error(name .. " must be an array")
|
|
157
|
+
end
|
|
158
|
+
if maxItems and count > maxItems then
|
|
159
|
+
error(name .. " must contain at most " .. tostring(maxItems) .. " items")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
local function requirePhotoIdArray(value, name, maxItems)
|
|
164
|
+
if type(value) ~= "table" then
|
|
165
|
+
error(name .. " is required")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
local normalized = {}
|
|
169
|
+
local count = 0
|
|
170
|
+
local maxIndex = 0
|
|
171
|
+
for key, item in pairs(value) do
|
|
172
|
+
if type(key) ~= "number" or key < 1 or key ~= math.floor(key) then
|
|
173
|
+
error(name .. " must be an array")
|
|
174
|
+
end
|
|
175
|
+
if type(item) == "number" then
|
|
176
|
+
normalized[key] = tostring(item)
|
|
177
|
+
elseif type(item) == "string" and item ~= "" then
|
|
178
|
+
normalized[key] = item
|
|
179
|
+
else
|
|
180
|
+
error(name .. "[" .. tostring(key) .. "] must be a photo id or file path")
|
|
181
|
+
end
|
|
182
|
+
count = count + 1
|
|
183
|
+
if key > maxIndex then maxIndex = key end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if count == 0 then
|
|
187
|
+
error(name .. " is required")
|
|
188
|
+
end
|
|
189
|
+
if count ~= maxIndex then
|
|
190
|
+
error(name .. " must be an array")
|
|
191
|
+
end
|
|
192
|
+
if maxItems and count > maxItems then
|
|
193
|
+
error(name .. " must contain at most " .. tostring(maxItems) .. " items")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
return normalized
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
local function requireAllowedDevelopSettingKey(key)
|
|
200
|
+
if not ALLOWED_DEVELOP_SETTING_LOOKUP[key] then
|
|
201
|
+
error("Unsupported develop setting key: " .. tostring(key))
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
local function requirePointCurve(value, name)
|
|
206
|
+
if type(value) ~= "table" then
|
|
207
|
+
error(name .. " must be an array of input/output number pairs")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
local count = 0
|
|
211
|
+
for index, item in pairs(value) do
|
|
212
|
+
if type(index) ~= "number" or index < 1 or index ~= math.floor(index) then
|
|
213
|
+
error(name .. " must be an array")
|
|
214
|
+
end
|
|
215
|
+
if type(item) ~= "number" or item < 0 or item > 255 or item ~= math.floor(item) then
|
|
216
|
+
error(name .. "[" .. tostring(index) .. "] must be an integer from 0 to 255")
|
|
217
|
+
end
|
|
218
|
+
count = count + 1
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
if count ~= #value then
|
|
222
|
+
error(name .. " must be an array")
|
|
223
|
+
end
|
|
224
|
+
if count < 4 or count > MAX_POINT_CURVE_VALUES or count % 2 ~= 0 then
|
|
225
|
+
error(name .. " must contain 2 to 32 input/output pairs")
|
|
226
|
+
end
|
|
227
|
+
if value[1] ~= 0 or value[count - 1] ~= 255 then
|
|
228
|
+
error(name .. " must start at input 0 and end at input 255")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
local previousInput = nil
|
|
232
|
+
for index = 1, count, 2 do
|
|
233
|
+
local input = value[index]
|
|
234
|
+
if previousInput ~= nil and input <= previousInput then
|
|
235
|
+
error(name .. " input values must be strictly increasing")
|
|
236
|
+
end
|
|
237
|
+
previousInput = input
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
local function requireDevelopSettingValue(key, value)
|
|
242
|
+
if POINT_CURVE_SETTING_LOOKUP[key] then
|
|
243
|
+
requirePointCurve(value, key)
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
local valueType = type(value)
|
|
248
|
+
if valueType ~= "number" and valueType ~= "string" and valueType ~= "boolean" then
|
|
249
|
+
error("Unsupported value for develop setting key: " .. tostring(key))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
local function requireCapturedSettingValue(key, value)
|
|
254
|
+
local valueType = type(value)
|
|
255
|
+
if valueType == "number" or valueType == "string" or valueType == "boolean" then
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
if POINT_CURVE_SETTING_LOOKUP[key] and valueType == "table" then
|
|
259
|
+
local count = 0
|
|
260
|
+
for index, item in pairs(value) do
|
|
261
|
+
if type(index) ~= "number" or index < 1 or index ~= math.floor(index) then
|
|
262
|
+
error("Develop setting " .. tostring(key) .. " is not a capturable array")
|
|
263
|
+
end
|
|
264
|
+
if type(item) ~= "number" then
|
|
265
|
+
error("Develop setting " .. tostring(key) .. " must contain only numbers")
|
|
266
|
+
end
|
|
267
|
+
count = count + 1
|
|
268
|
+
end
|
|
269
|
+
if count ~= #value or count % 2 ~= 0 or count < 4 or count > MAX_POINT_CURVE_VALUES then
|
|
270
|
+
error("Develop setting " .. tostring(key) .. " is not a capturable point curve")
|
|
271
|
+
end
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
error("Unsupported value for develop setting key: " .. tostring(key))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
local function requireDevelopSettingsObject(settings)
|
|
278
|
+
if type(settings) ~= "table" then
|
|
279
|
+
error("settings is required")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
local count = 0
|
|
283
|
+
for key, value in pairs(settings) do
|
|
284
|
+
if type(key) ~= "string" then
|
|
285
|
+
error("settings keys must be strings")
|
|
286
|
+
end
|
|
287
|
+
requireAllowedDevelopSettingKey(key)
|
|
288
|
+
requireDevelopSettingValue(key, value)
|
|
289
|
+
count = count + 1
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
if count == 0 then
|
|
293
|
+
error("settings is required")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
local function requireDevelopSettingWhitelist(settings)
|
|
298
|
+
if settings == nil then
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
requireStringArray(settings, "settings", #ALLOWED_DEVELOP_SETTING_KEYS)
|
|
303
|
+
for _, key in ipairs(settings) do
|
|
304
|
+
requireAllowedDevelopSettingKey(key)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
local function callPresetMethod(preset, methodName)
|
|
309
|
+
local method = preset and preset[methodName]
|
|
310
|
+
if type(method) ~= "function" then
|
|
311
|
+
return nil
|
|
312
|
+
end
|
|
313
|
+
local ok, value = pcall(method, preset)
|
|
314
|
+
if not ok then
|
|
315
|
+
return nil
|
|
316
|
+
end
|
|
317
|
+
return value
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
local function presetEntry(preset, folder, scope)
|
|
321
|
+
return {
|
|
322
|
+
preset = preset,
|
|
323
|
+
name = callPresetMethod(preset, "getName"),
|
|
324
|
+
folder = folder,
|
|
325
|
+
scope = scope,
|
|
326
|
+
uuid = callPresetMethod(preset, "getUuid"),
|
|
327
|
+
file = callPresetMethod(preset, "getFile"),
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
local function allDevelopPresetEntries()
|
|
332
|
+
local entries = {}
|
|
333
|
+
for _, folder in ipairs(LrApplication.developPresetFolders()) do
|
|
334
|
+
for _, preset in ipairs(folder:getDevelopPresets()) do
|
|
335
|
+
table.insert(entries, presetEntry(preset, folder:getName(), "lightroom"))
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
if _PLUGIN and type(LrApplication.getDevelopPresetsForPlugin) == "function" then
|
|
340
|
+
local pluginPresets = LrApplication.getDevelopPresetsForPlugin(_PLUGIN) or {}
|
|
341
|
+
for _, preset in ipairs(pluginPresets) do
|
|
342
|
+
table.insert(entries, presetEntry(preset, "Plugin Develop Presets", "plugin"))
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
return entries
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
local function presetSummary(entry)
|
|
350
|
+
return {
|
|
351
|
+
name = entry.name,
|
|
352
|
+
folder = entry.folder,
|
|
353
|
+
scope = entry.scope,
|
|
354
|
+
uuid = entry.uuid,
|
|
355
|
+
file = entry.file,
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
local function requirePresetSelector(args)
|
|
360
|
+
if type(args) ~= "table" then
|
|
361
|
+
error("preset selector is required")
|
|
362
|
+
end
|
|
363
|
+
local hasName = type(args.preset_name) == "string" and args.preset_name ~= ""
|
|
364
|
+
local hasUuid = type(args.preset_uuid) == "string" and args.preset_uuid ~= ""
|
|
365
|
+
if not hasName and not hasUuid then
|
|
366
|
+
error("preset_name or preset_uuid is required")
|
|
367
|
+
end
|
|
368
|
+
if args.preset_folder ~= nil then requireString(args.preset_folder, "preset_folder") end
|
|
369
|
+
if args.preset_scope ~= nil and args.preset_scope ~= "lightroom" and args.preset_scope ~= "plugin" then
|
|
370
|
+
error("preset_scope must be lightroom or plugin")
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
local function sharesOneUuid(matches)
|
|
375
|
+
local uuid = matches[1].uuid
|
|
376
|
+
if uuid == nil then
|
|
377
|
+
return false
|
|
378
|
+
end
|
|
379
|
+
for index = 2, #matches do
|
|
380
|
+
if matches[index].uuid ~= uuid then
|
|
381
|
+
return false
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
return true
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
local function findPreset(args)
|
|
388
|
+
requirePresetSelector(args)
|
|
389
|
+
local matches = {}
|
|
390
|
+
for _, entry in ipairs(allDevelopPresetEntries()) do
|
|
391
|
+
local uuidMatches = args.preset_uuid == nil or entry.uuid == args.preset_uuid
|
|
392
|
+
local nameMatches = args.preset_name == nil or entry.name == args.preset_name
|
|
393
|
+
local folderMatches = args.preset_folder == nil or entry.folder == args.preset_folder
|
|
394
|
+
local scopeMatches = args.preset_scope == nil or entry.scope == args.preset_scope
|
|
395
|
+
if uuidMatches and nameMatches and folderMatches and scopeMatches then
|
|
396
|
+
table.insert(matches, entry)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
if #matches == 0 then
|
|
400
|
+
error("Preset not found")
|
|
401
|
+
end
|
|
402
|
+
-- Lightroom can expose the same preset more than once (for example, as a
|
|
403
|
+
-- favourite and in its original group). Those aliases share one UUID, so
|
|
404
|
+
-- a UUID selector is still exact even when the flattened folder list has
|
|
405
|
+
-- multiple entries for it.
|
|
406
|
+
if #matches > 1 and args.preset_uuid == nil and not sharesOneUuid(matches) then
|
|
407
|
+
error("Preset selector is ambiguous; provide preset_uuid or preset_folder")
|
|
408
|
+
end
|
|
409
|
+
return matches[1]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
local function cloneSerializable(value, depth, seen)
|
|
413
|
+
local valueType = type(value)
|
|
414
|
+
if valueType == "number" or valueType == "string" or valueType == "boolean" then
|
|
415
|
+
return value, true
|
|
416
|
+
end
|
|
417
|
+
if valueType ~= "table" or depth >= 6 or seen[value] then
|
|
418
|
+
return nil, false
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
seen[value] = true
|
|
422
|
+
local out = {}
|
|
423
|
+
local count = 0
|
|
424
|
+
for key, item in pairs(value) do
|
|
425
|
+
if type(key) ~= "string" and type(key) ~= "number" then
|
|
426
|
+
seen[value] = nil
|
|
427
|
+
return nil, false
|
|
428
|
+
end
|
|
429
|
+
count = count + 1
|
|
430
|
+
if count > 2000 then
|
|
431
|
+
seen[value] = nil
|
|
432
|
+
return nil, false
|
|
433
|
+
end
|
|
434
|
+
local cloned, supported = cloneSerializable(item, depth + 1, seen)
|
|
435
|
+
if not supported then
|
|
436
|
+
seen[value] = nil
|
|
437
|
+
return nil, false
|
|
438
|
+
end
|
|
439
|
+
out[key] = cloned
|
|
440
|
+
end
|
|
441
|
+
seen[value] = nil
|
|
442
|
+
return out, true
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
local function normalizedPresetSettings(preset)
|
|
446
|
+
local settings = callPresetMethod(preset, "getSetting")
|
|
447
|
+
if type(settings) ~= "table" then
|
|
448
|
+
error("Preset settings are unavailable")
|
|
449
|
+
end
|
|
450
|
+
local normalized, supported = cloneSerializable(settings, 0, {})
|
|
451
|
+
if not supported then
|
|
452
|
+
error("Preset settings contain unsupported nested values")
|
|
453
|
+
end
|
|
454
|
+
return normalized
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
-- Lightroom mints a fresh CorrectionID/MaskID on every getSetting() call, so
|
|
458
|
+
-- they identify a read rather than the preset. Comparing them made every
|
|
459
|
+
-- masked preset (all the Adaptive/AI ones) differ from itself.
|
|
460
|
+
local VOLATILE_SETTING_KEYS = {
|
|
461
|
+
CorrectionID = true,
|
|
462
|
+
MaskID = true,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
local function withoutVolatileIds(value)
|
|
466
|
+
if type(value) ~= "table" then return value end
|
|
467
|
+
local out = {}
|
|
468
|
+
for key, item in pairs(value) do
|
|
469
|
+
if not VOLATILE_SETTING_KEYS[key] then
|
|
470
|
+
out[key] = withoutVolatileIds(item)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
return out
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
local function deepEqual(left, right)
|
|
477
|
+
if type(left) ~= type(right) then return false end
|
|
478
|
+
if type(left) ~= "table" then return left == right end
|
|
479
|
+
for key, value in pairs(left) do
|
|
480
|
+
if not deepEqual(value, right[key]) then return false end
|
|
481
|
+
end
|
|
482
|
+
for key, _ in pairs(right) do
|
|
483
|
+
if left[key] == nil then return false end
|
|
484
|
+
end
|
|
485
|
+
return true
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
local function sortedSettingKeys(settings)
|
|
489
|
+
local keys = {}
|
|
490
|
+
for key, _ in pairs(settings) do table.insert(keys, key) end
|
|
491
|
+
table.sort(keys)
|
|
492
|
+
return keys
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
function DevelopHandler.listDevelopPresets(_)
|
|
496
|
+
local out = {}
|
|
497
|
+
for _, entry in ipairs(allDevelopPresetEntries()) do
|
|
498
|
+
table.insert(out, presetSummary(entry))
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
Log.info(string.format("Listed %d develop presets", #out))
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
success = true,
|
|
505
|
+
presets = out,
|
|
506
|
+
count = #out,
|
|
507
|
+
}
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
function DevelopHandler.getDevelopPreset(args)
|
|
511
|
+
local entry = findPreset(args)
|
|
512
|
+
local settings = normalizedPresetSettings(entry.preset)
|
|
513
|
+
local result = presetSummary(entry)
|
|
514
|
+
result.success = true
|
|
515
|
+
result.settings = settings
|
|
516
|
+
result.setting_count = #sortedSettingKeys(settings)
|
|
517
|
+
return result
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
function DevelopHandler.compareDevelopPresets(args)
|
|
521
|
+
if type(args.base) ~= "table" or type(args.candidate) ~= "table" then
|
|
522
|
+
error("base and candidate preset selectors are required")
|
|
523
|
+
end
|
|
524
|
+
local base = findPreset(args.base)
|
|
525
|
+
local candidate = findPreset(args.candidate)
|
|
526
|
+
local baseSettings = normalizedPresetSettings(base.preset)
|
|
527
|
+
local candidateSettings = normalizedPresetSettings(candidate.preset)
|
|
528
|
+
local keys = {}
|
|
529
|
+
for key, _ in pairs(baseSettings) do keys[key] = true end
|
|
530
|
+
for key, _ in pairs(candidateSettings) do keys[key] = true end
|
|
531
|
+
|
|
532
|
+
local keyList = {}
|
|
533
|
+
for key, _ in pairs(keys) do table.insert(keyList, key) end
|
|
534
|
+
table.sort(keyList)
|
|
535
|
+
|
|
536
|
+
local changes = {}
|
|
537
|
+
for _, key in ipairs(keyList) do
|
|
538
|
+
local before = withoutVolatileIds(baseSettings[key])
|
|
539
|
+
local after = withoutVolatileIds(candidateSettings[key])
|
|
540
|
+
if not deepEqual(before, after) then
|
|
541
|
+
local change = {
|
|
542
|
+
key = key,
|
|
543
|
+
before_present = baseSettings[key] ~= nil,
|
|
544
|
+
after_present = candidateSettings[key] ~= nil,
|
|
545
|
+
}
|
|
546
|
+
if before ~= nil then change.before = before end
|
|
547
|
+
if after ~= nil then change.after = after end
|
|
548
|
+
table.insert(changes, change)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
success = true,
|
|
554
|
+
base = presetSummary(base),
|
|
555
|
+
candidate = presetSummary(candidate),
|
|
556
|
+
changes = changes,
|
|
557
|
+
changed_count = #changes,
|
|
558
|
+
}
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
function DevelopHandler.createDevelopPreset(args)
|
|
562
|
+
args.photo_id = requirePhotoId(args.photo_id, "photo_id")
|
|
563
|
+
requireString(args.preset_name, "preset_name")
|
|
564
|
+
requireStringArray(args.settings, "settings", #ALLOWED_DEVELOP_SETTING_KEYS)
|
|
565
|
+
requireDevelopSettingWhitelist(args.settings)
|
|
566
|
+
|
|
567
|
+
if not _PLUGIN or type(LrApplication.addDevelopPresetForPlugin) ~= "function" then
|
|
568
|
+
error("Plugin preset creation is unavailable")
|
|
569
|
+
end
|
|
570
|
+
for _, entry in ipairs(allDevelopPresetEntries()) do
|
|
571
|
+
if entry.scope == "plugin" and entry.name == args.preset_name then
|
|
572
|
+
error("Plugin preset already exists; use a versioned preset_name")
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
local catalog = LrApplication.activeCatalog()
|
|
577
|
+
local sourceSettings
|
|
578
|
+
catalog:withReadAccessDo(function()
|
|
579
|
+
local photo = PhotoLookup.resolveOne(catalog, args.photo_id)
|
|
580
|
+
if not photo then
|
|
581
|
+
error("Photo not found: " .. args.photo_id)
|
|
582
|
+
end
|
|
583
|
+
sourceSettings = photo:getDevelopSettings()
|
|
584
|
+
end)
|
|
585
|
+
|
|
586
|
+
local presetSettings = {}
|
|
587
|
+
for _, key in ipairs(args.settings) do
|
|
588
|
+
local value = sourceSettings[key]
|
|
589
|
+
if value == nil then
|
|
590
|
+
error("Source photo has no develop setting: " .. key)
|
|
591
|
+
end
|
|
592
|
+
requireCapturedSettingValue(key, value)
|
|
593
|
+
presetSettings[key] = value
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
local preset = LrApplication.addDevelopPresetForPlugin(_PLUGIN, args.preset_name, presetSettings)
|
|
597
|
+
if not preset then error("Lightroom did not create the plugin preset") end
|
|
598
|
+
local entry = presetEntry(preset, "Plugin Develop Presets", "plugin")
|
|
599
|
+
local result = presetSummary(entry)
|
|
600
|
+
result.success = true
|
|
601
|
+
result.source_photo_id = args.photo_id
|
|
602
|
+
result.settings = args.settings
|
|
603
|
+
result.visible_in_develop = false
|
|
604
|
+
result.message = "Created plugin-managed Develop preset checkpoint"
|
|
605
|
+
Log.info(string.format("Created plugin preset %s from photo %s", args.preset_name, args.photo_id))
|
|
606
|
+
return result
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
local function requireLeafFilename(filename)
|
|
610
|
+
requireString(filename, "filename")
|
|
611
|
+
if filename == "." or filename == ".." or LrPathUtils.leafName(filename) ~= filename then
|
|
612
|
+
error("filename must be a leaf filename without path separators")
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
function DevelopHandler.exportDevelopPreset(args)
|
|
617
|
+
requireString(args.destination_dir, "destination_dir")
|
|
618
|
+
local entry = findPreset(args)
|
|
619
|
+
local source = entry.file
|
|
620
|
+
if type(source) ~= "string" or source == "" or LrFileUtils.exists(source) ~= "file" then
|
|
621
|
+
error("Preset has no exportable backing file")
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
local sourceExtension = LrPathUtils.extension(source)
|
|
625
|
+
if type(sourceExtension) ~= "string" or sourceExtension == "" then
|
|
626
|
+
error("Preset backing file has no extension")
|
|
627
|
+
end
|
|
628
|
+
local filename = args.filename or LrPathUtils.leafName(source)
|
|
629
|
+
requireLeafFilename(filename)
|
|
630
|
+
local requestedExtension = LrPathUtils.extension(filename)
|
|
631
|
+
if requestedExtension == "" then
|
|
632
|
+
filename = filename .. "." .. sourceExtension
|
|
633
|
+
elseif requestedExtension:lower() ~= sourceExtension:lower() then
|
|
634
|
+
error("filename extension must match preset backing file: ." .. sourceExtension)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
if LrFileUtils.exists(args.destination_dir) == false then
|
|
638
|
+
LrFileUtils.createAllDirectories(args.destination_dir)
|
|
639
|
+
end
|
|
640
|
+
if LrFileUtils.exists(args.destination_dir) ~= "directory" then
|
|
641
|
+
error("destination_dir is not a directory")
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
local destination = LrPathUtils.child(args.destination_dir, filename)
|
|
645
|
+
if LrFileUtils.exists(destination) then
|
|
646
|
+
error("destination preset already exists; choose a new filename")
|
|
647
|
+
end
|
|
648
|
+
local copied, copyError = LrFileUtils.copy(source, destination)
|
|
649
|
+
if not copied then
|
|
650
|
+
error("Preset export failed: " .. tostring(copyError))
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
local result = presetSummary(entry)
|
|
654
|
+
result.success = true
|
|
655
|
+
result.destination = destination
|
|
656
|
+
result.message = "Exported Develop preset without overwriting existing files"
|
|
657
|
+
Log.info(string.format("Exported preset %s to %s", tostring(entry.name), destination))
|
|
658
|
+
return result
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
function DevelopHandler.applyDevelopPreset(args)
|
|
662
|
+
args.photo_ids = requirePhotoIdArray(args.photo_ids, "photo_ids", MAX_BULK_PHOTO_IDS)
|
|
663
|
+
local selectedPreset = findPreset(args)
|
|
664
|
+
|
|
665
|
+
local catalog = LrApplication.activeCatalog()
|
|
666
|
+
local appliedCount = 0
|
|
667
|
+
|
|
668
|
+
local missingIds = {}
|
|
669
|
+
local missingCount = 0
|
|
670
|
+
|
|
671
|
+
catalog:withWriteAccessDo("Apply Develop Preset", function()
|
|
672
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
|
|
673
|
+
for _, resolvedEntry in ipairs(resolved) do
|
|
674
|
+
if resolvedEntry.photo then
|
|
675
|
+
if selectedPreset.scope == "plugin" then
|
|
676
|
+
resolvedEntry.photo:applyDevelopPreset(selectedPreset.preset, _PLUGIN)
|
|
677
|
+
else
|
|
678
|
+
resolvedEntry.photo:applyDevelopPreset(selectedPreset.preset)
|
|
679
|
+
end
|
|
680
|
+
appliedCount = appliedCount + 1
|
|
681
|
+
else
|
|
682
|
+
missingCount = missingCount + 1
|
|
683
|
+
missingIds[missingCount] = tostring(resolvedEntry.id)
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
end)
|
|
687
|
+
|
|
688
|
+
Log.info(string.format("Applied preset %s to %d photos", selectedPreset.name, appliedCount))
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
success = true,
|
|
692
|
+
applied = appliedCount,
|
|
693
|
+
preset = selectedPreset.name,
|
|
694
|
+
folder = selectedPreset.folder,
|
|
695
|
+
scope = selectedPreset.scope,
|
|
696
|
+
uuid = selectedPreset.uuid,
|
|
697
|
+
missing = missingIds,
|
|
698
|
+
message = string.format("Applied preset %s to %d photos (%d ids not found)",
|
|
699
|
+
selectedPreset.name, appliedCount, missingCount),
|
|
700
|
+
}
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
function DevelopHandler.copyDevelopSettings(args)
|
|
704
|
+
args.source_id = requirePhotoId(args.source_id, "source_id")
|
|
705
|
+
args.target_ids = requirePhotoIdArray(args.target_ids, "target_ids", MAX_BULK_PHOTO_IDS)
|
|
706
|
+
requireDevelopSettingWhitelist(args.settings)
|
|
707
|
+
|
|
708
|
+
local catalog = LrApplication.activeCatalog()
|
|
709
|
+
local sourceSettings
|
|
710
|
+
|
|
711
|
+
catalog:withReadAccessDo(function()
|
|
712
|
+
local source = PhotoLookup.resolveOne(catalog, args.source_id)
|
|
713
|
+
if not source then
|
|
714
|
+
error("Source photo not found: " .. args.source_id)
|
|
715
|
+
end
|
|
716
|
+
sourceSettings = source:getDevelopSettings()
|
|
717
|
+
end)
|
|
718
|
+
|
|
719
|
+
local toApply = sourceSettings
|
|
720
|
+
if args.settings then
|
|
721
|
+
toApply = {}
|
|
722
|
+
for _, key in ipairs(args.settings) do
|
|
723
|
+
toApply[key] = sourceSettings[key]
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
local copiedCount = 0
|
|
728
|
+
local missingIds = {}
|
|
729
|
+
local missingCount = 0
|
|
730
|
+
|
|
731
|
+
catalog:withWriteAccessDo("Copy Develop Settings", function()
|
|
732
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.target_ids)
|
|
733
|
+
for _, entry in ipairs(resolved) do
|
|
734
|
+
if entry.photo then
|
|
735
|
+
entry.photo:applyDevelopSettings(toApply)
|
|
736
|
+
copiedCount = copiedCount + 1
|
|
737
|
+
else
|
|
738
|
+
missingCount = missingCount + 1
|
|
739
|
+
missingIds[missingCount] = tostring(entry.id)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end)
|
|
743
|
+
|
|
744
|
+
Log.info(string.format("Copied develop settings from %s to %d photos", args.source_id, copiedCount))
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
success = true,
|
|
748
|
+
copied = copiedCount,
|
|
749
|
+
source = args.source_id,
|
|
750
|
+
missing = missingIds,
|
|
751
|
+
message = string.format("Copied develop settings from %s to %d photos (%d ids not found)",
|
|
752
|
+
args.source_id, copiedCount, missingCount),
|
|
753
|
+
}
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
function DevelopHandler.setDevelopSettings(args)
|
|
757
|
+
args.photo_id = requirePhotoId(args.photo_id, "photo_id")
|
|
758
|
+
requireDevelopSettingsObject(args.settings)
|
|
759
|
+
|
|
760
|
+
local catalog = LrApplication.activeCatalog()
|
|
761
|
+
local applied = false
|
|
762
|
+
|
|
763
|
+
catalog:withWriteAccessDo("Set Develop Settings", function()
|
|
764
|
+
local photo = PhotoLookup.resolveOne(catalog, args.photo_id)
|
|
765
|
+
if not photo then
|
|
766
|
+
error("Photo not found: " .. args.photo_id)
|
|
767
|
+
end
|
|
768
|
+
photo:applyDevelopSettings(args.settings)
|
|
769
|
+
applied = true
|
|
770
|
+
end)
|
|
771
|
+
|
|
772
|
+
Log.info(string.format("Set develop settings on photo %s", args.photo_id))
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
success = applied,
|
|
776
|
+
photo_id = args.photo_id,
|
|
777
|
+
}
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
-- White balance presets accepted by the WhiteBalance develop setting. "Auto"
|
|
781
|
+
-- is resolved immediately thanks to applyDevelopSettings' optFlattenAutoNow
|
|
782
|
+
-- (SDK 13+; older versions resolve lazily on the next Develop render).
|
|
783
|
+
local WHITE_BALANCE_PRESETS = {
|
|
784
|
+
["As Shot"] = true,
|
|
785
|
+
["Auto"] = true,
|
|
786
|
+
["Daylight"] = true,
|
|
787
|
+
["Cloudy"] = true,
|
|
788
|
+
["Shade"] = true,
|
|
789
|
+
["Tungsten"] = true,
|
|
790
|
+
["Fluorescent"] = true,
|
|
791
|
+
["Flash"] = true,
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function DevelopHandler.setWhiteBalance(args)
|
|
795
|
+
if not args.photo_ids or #args.photo_ids == 0 then
|
|
796
|
+
error("photo_ids is required")
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
local settings = {}
|
|
800
|
+
|
|
801
|
+
if args.preset ~= nil then
|
|
802
|
+
if not WHITE_BALANCE_PRESETS[args.preset] then
|
|
803
|
+
error("preset must be one of: As Shot, Auto, Daylight, Cloudy, Shade, Tungsten, Fluorescent, Flash")
|
|
804
|
+
end
|
|
805
|
+
settings.WhiteBalance = args.preset
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
if args.temperature ~= nil or args.tint ~= nil then
|
|
809
|
+
settings.WhiteBalance = "Custom"
|
|
810
|
+
if args.temperature ~= nil then
|
|
811
|
+
if type(args.temperature) ~= "number" or args.temperature < 2000 or args.temperature > 50000 then
|
|
812
|
+
error("temperature must be a Kelvin number between 2000 and 50000")
|
|
813
|
+
end
|
|
814
|
+
settings.Temperature = args.temperature
|
|
815
|
+
end
|
|
816
|
+
if args.tint ~= nil then
|
|
817
|
+
if type(args.tint) ~= "number" or args.tint < -150 or args.tint > 150 then
|
|
818
|
+
error("tint must be a number between -150 (green) and 150 (magenta)")
|
|
819
|
+
end
|
|
820
|
+
settings.Tint = args.tint
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
if next(settings) == nil then
|
|
825
|
+
error("provide a preset, or temperature and/or tint")
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
local catalog = LrApplication.activeCatalog()
|
|
829
|
+
local updatedCount = 0
|
|
830
|
+
local nonRaw = 0
|
|
831
|
+
local missingIds = {}
|
|
832
|
+
local missingCount = 0
|
|
833
|
+
|
|
834
|
+
catalog:withWriteAccessDo("Set White Balance", function()
|
|
835
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
|
|
836
|
+
for _, entry in ipairs(resolved) do
|
|
837
|
+
local photo = entry.photo
|
|
838
|
+
if photo then
|
|
839
|
+
-- optFlattenAutoNow resolves "Auto" synchronously when the
|
|
840
|
+
-- SDK supports it (3rd parameter, SDK 13+).
|
|
841
|
+
photo:applyDevelopSettings(settings, "MCP White Balance", true)
|
|
842
|
+
updatedCount = updatedCount + 1
|
|
843
|
+
local format = photo:getRawMetadata('fileFormat')
|
|
844
|
+
if format ~= "RAW" and format ~= "DNG" then
|
|
845
|
+
nonRaw = nonRaw + 1
|
|
846
|
+
end
|
|
847
|
+
else
|
|
848
|
+
missingCount = missingCount + 1
|
|
849
|
+
missingIds[missingCount] = tostring(entry.id)
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
end)
|
|
853
|
+
|
|
854
|
+
-- Read back the effective values of the first photo so the caller can
|
|
855
|
+
-- verify what Lightroom actually stored.
|
|
856
|
+
local verified = nil
|
|
857
|
+
if updatedCount > 0 then
|
|
858
|
+
catalog:withReadAccessDo(function()
|
|
859
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_ids[1] })
|
|
860
|
+
if resolved[1] and resolved[1].photo then
|
|
861
|
+
local current = resolved[1].photo:getDevelopSettings()
|
|
862
|
+
verified = {
|
|
863
|
+
WhiteBalance = current.WhiteBalance,
|
|
864
|
+
Temperature = current.Temperature,
|
|
865
|
+
Tint = current.Tint,
|
|
866
|
+
}
|
|
867
|
+
end
|
|
868
|
+
end)
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
Log.info(string.format("Set white balance for %d photos", updatedCount))
|
|
872
|
+
|
|
873
|
+
local result = {
|
|
874
|
+
success = true,
|
|
875
|
+
updated = updatedCount,
|
|
876
|
+
missing = missingIds,
|
|
877
|
+
verified = verified,
|
|
878
|
+
message = string.format("Set white balance on %d photos (%d ids not found)",
|
|
879
|
+
updatedCount, missingCount),
|
|
880
|
+
}
|
|
881
|
+
if nonRaw > 0 then
|
|
882
|
+
result.warning = string.format(
|
|
883
|
+
"%d photo(s) are not RAW/DNG: white balance has limited effect on JPEG/TIFF/PSD files.", nonRaw)
|
|
884
|
+
end
|
|
885
|
+
return result
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
-- =====================================================================
|
|
889
|
+
-- Tone curve (PV2012 point curves)
|
|
890
|
+
-- =====================================================================
|
|
891
|
+
--
|
|
892
|
+
-- Lightroom stores each point curve as a flat array of x,y pairs in
|
|
893
|
+
-- 0..255 input/output space: { 0,0, 128,140, 255,255 }. The dedicated
|
|
894
|
+
-- set_tone_curve / get_tone_curve tools wrap that flat format with a
|
|
895
|
+
-- friendlier [[x,y], ...] shape, endpoint normalization, monotonic-x
|
|
896
|
+
-- validation and read-back verification.
|
|
897
|
+
|
|
898
|
+
local TONE_CURVE_CHANNELS = {
|
|
899
|
+
main = "ToneCurvePV2012",
|
|
900
|
+
red = "ToneCurvePV2012Red",
|
|
901
|
+
green = "ToneCurvePV2012Green",
|
|
902
|
+
blue = "ToneCurvePV2012Blue",
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
-- Approximations of the built-in Lightroom curve presets; the UI name is
|
|
906
|
+
-- reported alongside so Lightroom shows the matching dropdown entry.
|
|
907
|
+
local TONE_CURVE_PRESETS = {
|
|
908
|
+
linear = {
|
|
909
|
+
name = "Linear",
|
|
910
|
+
points = { { 0, 0 }, { 255, 255 } },
|
|
911
|
+
},
|
|
912
|
+
medium_contrast = {
|
|
913
|
+
name = "Medium Contrast",
|
|
914
|
+
points = { { 0, 0 }, { 64, 56 }, { 128, 128 }, { 192, 202 }, { 255, 255 } },
|
|
915
|
+
},
|
|
916
|
+
strong_contrast = {
|
|
917
|
+
name = "Strong Contrast",
|
|
918
|
+
points = { { 0, 0 }, { 64, 42 }, { 128, 128 }, { 192, 216 }, { 255, 255 } },
|
|
919
|
+
},
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
local MAX_CURVE_POINTS = 32
|
|
923
|
+
|
|
924
|
+
local function parseCurveToPoints(curveArray)
|
|
925
|
+
local points = {}
|
|
926
|
+
if type(curveArray) == "table" then
|
|
927
|
+
for i = 1, #curveArray, 2 do
|
|
928
|
+
table.insert(points, { curveArray[i], curveArray[i + 1] })
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
return points
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
local function buildCurveArray(points)
|
|
935
|
+
local flat = {}
|
|
936
|
+
for _, p in ipairs(points) do
|
|
937
|
+
table.insert(flat, p[1])
|
|
938
|
+
table.insert(flat, p[2])
|
|
939
|
+
end
|
|
940
|
+
return flat
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
-- Validate + normalize caller-supplied points. Lightroom expects the curve
|
|
944
|
+
-- to start at (0,0) and end at (255,255) with strictly increasing x, so
|
|
945
|
+
-- missing endpoints are inserted (and reported) rather than rejected.
|
|
946
|
+
local function normalizeCurvePoints(points)
|
|
947
|
+
if type(points) ~= "table" or #points < 1 then
|
|
948
|
+
error("points must be an array of at least 1 [x, y] pair")
|
|
949
|
+
end
|
|
950
|
+
if #points > MAX_CURVE_POINTS then
|
|
951
|
+
error("points must contain at most " .. MAX_CURVE_POINTS .. " points")
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
local normalized = {}
|
|
955
|
+
local endpointsAdded = false
|
|
956
|
+
for i, pair in ipairs(points) do
|
|
957
|
+
if type(pair) ~= "table" or #pair ~= 2
|
|
958
|
+
or type(pair[1]) ~= "number" or type(pair[2]) ~= "number" then
|
|
959
|
+
error("points[" .. i .. "] must be an [x, y] pair of numbers")
|
|
960
|
+
end
|
|
961
|
+
local x = math.floor(pair[1] + 0.5)
|
|
962
|
+
local y = math.floor(pair[2] + 0.5)
|
|
963
|
+
if x < 0 or x > 255 or y < 0 or y > 255 then
|
|
964
|
+
error("points[" .. i .. "] coordinates must be between 0 and 255")
|
|
965
|
+
end
|
|
966
|
+
if #normalized > 0 and x <= normalized[#normalized][1] then
|
|
967
|
+
error("points must have strictly increasing x values")
|
|
968
|
+
end
|
|
969
|
+
table.insert(normalized, { x, y })
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
if normalized[1][1] ~= 0 or normalized[1][2] ~= 0 then
|
|
973
|
+
table.insert(normalized, 1, { 0, 0 })
|
|
974
|
+
endpointsAdded = true
|
|
975
|
+
end
|
|
976
|
+
if normalized[#normalized][1] ~= 255 or normalized[#normalized][2] ~= 255 then
|
|
977
|
+
if normalized[#normalized][1] >= 255 then
|
|
978
|
+
error("the last point must end at (255, 255)")
|
|
979
|
+
end
|
|
980
|
+
table.insert(normalized, { 255, 255 })
|
|
981
|
+
endpointsAdded = true
|
|
982
|
+
end
|
|
983
|
+
return normalized, endpointsAdded
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
function DevelopHandler.setToneCurve(args)
|
|
987
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
988
|
+
error("photo_id is required")
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
local channel = args.channel or "main"
|
|
992
|
+
local key = TONE_CURVE_CHANNELS[channel]
|
|
993
|
+
if not key then
|
|
994
|
+
error("channel must be one of: main, red, green, blue")
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
local points, endpointsAdded, curveName
|
|
998
|
+
if args.preset ~= nil then
|
|
999
|
+
local preset = TONE_CURVE_PRESETS[args.preset]
|
|
1000
|
+
if not preset then
|
|
1001
|
+
error("preset must be one of: linear, medium_contrast, strong_contrast")
|
|
1002
|
+
end
|
|
1003
|
+
curveName = preset.name
|
|
1004
|
+
endpointsAdded = false
|
|
1005
|
+
points = {}
|
|
1006
|
+
for i, p in ipairs(preset.points) do points[i] = { p[1], p[2] } end
|
|
1007
|
+
else
|
|
1008
|
+
points, endpointsAdded = normalizeCurvePoints(args.points)
|
|
1009
|
+
curveName = "Custom"
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
local catalog = LrApplication.activeCatalog()
|
|
1013
|
+
local updated = false
|
|
1014
|
+
local missing = true
|
|
1015
|
+
|
|
1016
|
+
catalog:withWriteAccessDo("Set Tone Curve", function()
|
|
1017
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1018
|
+
if resolved[1] and resolved[1].photo then
|
|
1019
|
+
missing = false
|
|
1020
|
+
resolved[1].photo:applyDevelopSettings({
|
|
1021
|
+
[key] = buildCurveArray(points),
|
|
1022
|
+
ToneCurveName2012 = curveName,
|
|
1023
|
+
}, "MCP Tone Curve")
|
|
1024
|
+
updated = true
|
|
1025
|
+
end
|
|
1026
|
+
end)
|
|
1027
|
+
|
|
1028
|
+
if missing then
|
|
1029
|
+
error("No photo matched photo_id")
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
-- Read back the stored curve so the caller can verify what Lightroom
|
|
1033
|
+
-- actually kept (values may be snapped by the tone engine).
|
|
1034
|
+
local verified = nil
|
|
1035
|
+
catalog:withReadAccessDo(function()
|
|
1036
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1037
|
+
if resolved[1] and resolved[1].photo then
|
|
1038
|
+
local current = resolved[1].photo:getDevelopSettings()
|
|
1039
|
+
verified = {
|
|
1040
|
+
curve_name = current.ToneCurveName2012,
|
|
1041
|
+
points = parseCurveToPoints(current[key]),
|
|
1042
|
+
raw = current[key],
|
|
1043
|
+
}
|
|
1044
|
+
end
|
|
1045
|
+
end)
|
|
1046
|
+
|
|
1047
|
+
Log.info(string.format("Set %s tone curve on photo %s",
|
|
1048
|
+
channel, tostring(args.photo_id)))
|
|
1049
|
+
|
|
1050
|
+
local result = {
|
|
1051
|
+
success = updated,
|
|
1052
|
+
channel = channel,
|
|
1053
|
+
curve_name = curveName,
|
|
1054
|
+
endpoints_added = endpointsAdded or nil,
|
|
1055
|
+
verified = verified,
|
|
1056
|
+
message = string.format("Set %s tone curve to '%s' (%d points)",
|
|
1057
|
+
channel, curveName, #points),
|
|
1058
|
+
}
|
|
1059
|
+
if endpointsAdded then
|
|
1060
|
+
result.message = result.message
|
|
1061
|
+
.. "; (0,0)/(255,255) endpoints were added automatically"
|
|
1062
|
+
end
|
|
1063
|
+
return result
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
function DevelopHandler.getToneCurve(args)
|
|
1067
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
1068
|
+
error("photo_id is required")
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
local catalog = LrApplication.activeCatalog()
|
|
1072
|
+
local out = nil
|
|
1073
|
+
|
|
1074
|
+
catalog:withReadAccessDo(function()
|
|
1075
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1076
|
+
if resolved[1] and resolved[1].photo then
|
|
1077
|
+
local s = resolved[1].photo:getDevelopSettings()
|
|
1078
|
+
local channels = {}
|
|
1079
|
+
for channel, key in pairs(TONE_CURVE_CHANNELS) do
|
|
1080
|
+
channels[channel] = {
|
|
1081
|
+
points = parseCurveToPoints(s[key]),
|
|
1082
|
+
raw = s[key],
|
|
1083
|
+
}
|
|
1084
|
+
end
|
|
1085
|
+
out = {
|
|
1086
|
+
curve_name = s.ToneCurveName2012,
|
|
1087
|
+
channels = channels,
|
|
1088
|
+
}
|
|
1089
|
+
end
|
|
1090
|
+
end)
|
|
1091
|
+
|
|
1092
|
+
if not out then
|
|
1093
|
+
error("No photo matched photo_id")
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
out.success = true
|
|
1097
|
+
out.message = string.format("Tone curves for photo %s (curve name: %s)",
|
|
1098
|
+
tostring(args.photo_id), tostring(out.curve_name))
|
|
1099
|
+
return out
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
-- =====================================================================
|
|
1103
|
+
-- apply_auto — official Auto Tone / Auto White Balance commands
|
|
1104
|
+
-- =====================================================================
|
|
1105
|
+
--
|
|
1106
|
+
-- LrDevelopController.setAutoTone() / setAutoWhiteBalance() are the same
|
|
1107
|
+
-- commands as the Auto button in the Develop panel: they run Lightroom's
|
|
1108
|
+
-- own analysis on the photo loaded in the Develop module. Because they
|
|
1109
|
+
-- drive the running UI, the handler switches to Develop once, selects
|
|
1110
|
+
-- each photo (outside any catalog gate), fires the commands, and diffs the
|
|
1111
|
+
-- main sliders before/after so the caller sees what actually changed.
|
|
1112
|
+
|
|
1113
|
+
local AUTO_READBACK_KEYS = {
|
|
1114
|
+
"Exposure2012", "Contrast2012", "Highlights2012", "Shadows2012",
|
|
1115
|
+
"Whites2012", "Blacks2012", "Temperature", "Tint",
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
local AUTO_SETTLE_AFTER_MODULE_SWITCH_S = 0.5
|
|
1119
|
+
local AUTO_SETTLE_BETWEEN_PHOTOS_S = 0.3
|
|
1120
|
+
local AUTO_SETTLE_AFTER_COMMAND_S = 0.5
|
|
1121
|
+
|
|
1122
|
+
function DevelopHandler.applyAuto(args)
|
|
1123
|
+
if not args.photo_ids or #args.photo_ids == 0 then
|
|
1124
|
+
error("photo_ids is required")
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
local operations = args.operations
|
|
1128
|
+
if operations == nil or #operations == 0 then
|
|
1129
|
+
operations = { "tone", "white_balance" }
|
|
1130
|
+
end
|
|
1131
|
+
local doTone, doWB = false, false
|
|
1132
|
+
for _, op in ipairs(operations) do
|
|
1133
|
+
if op == "tone" then
|
|
1134
|
+
doTone = true
|
|
1135
|
+
elseif op == "white_balance" then
|
|
1136
|
+
doWB = true
|
|
1137
|
+
else
|
|
1138
|
+
error("operations must contain only 'tone' and/or 'white_balance'")
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
if not doTone and not doWB then
|
|
1142
|
+
error("operations must include at least one of 'tone', 'white_balance'")
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
local catalog = LrApplication.activeCatalog()
|
|
1146
|
+
|
|
1147
|
+
local photos = {}
|
|
1148
|
+
catalog:withReadAccessDo(function()
|
|
1149
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
|
|
1150
|
+
for _, entry in ipairs(resolved) do
|
|
1151
|
+
if entry.photo then table.insert(photos, entry.photo) end
|
|
1152
|
+
end
|
|
1153
|
+
end)
|
|
1154
|
+
if #photos == 0 then
|
|
1155
|
+
error("No photos matched photo_ids")
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
-- switchToModule and setSelectedPhotos both yield to the UI thread, so
|
|
1159
|
+
-- they must run OUTSIDE any catalog access gate (#134/#124).
|
|
1160
|
+
local switchOk, switchErr = pcall(function()
|
|
1161
|
+
LrApplicationView.switchToModule("develop")
|
|
1162
|
+
end)
|
|
1163
|
+
if not switchOk then
|
|
1164
|
+
error("could not switch Lightroom to the Develop module: "
|
|
1165
|
+
.. tostring(switchErr))
|
|
1166
|
+
end
|
|
1167
|
+
LrTasks.sleep(AUTO_SETTLE_AFTER_MODULE_SWITCH_S)
|
|
1168
|
+
|
|
1169
|
+
local results = {}
|
|
1170
|
+
local succeeded = 0
|
|
1171
|
+
|
|
1172
|
+
for i, photo in ipairs(photos) do
|
|
1173
|
+
if i > 1 then LrTasks.sleep(AUTO_SETTLE_BETWEEN_PHOTOS_S) end
|
|
1174
|
+
|
|
1175
|
+
local entry = {
|
|
1176
|
+
photo = {
|
|
1177
|
+
id = photo.localIdentifier,
|
|
1178
|
+
path = photo:getRawMetadata('path'),
|
|
1179
|
+
filename = photo:getFormattedMetadata('fileName'),
|
|
1180
|
+
},
|
|
1181
|
+
applied = {},
|
|
1182
|
+
}
|
|
1183
|
+
table.insert(results, entry)
|
|
1184
|
+
|
|
1185
|
+
local selectOk = pcall(function()
|
|
1186
|
+
catalog:setSelectedPhotos(photo, { photo })
|
|
1187
|
+
end)
|
|
1188
|
+
if not selectOk then
|
|
1189
|
+
entry.error = "could not select the photo in Lightroom"
|
|
1190
|
+
else
|
|
1191
|
+
LrTasks.sleep(AUTO_SETTLE_BETWEEN_PHOTOS_S)
|
|
1192
|
+
|
|
1193
|
+
local before = nil
|
|
1194
|
+
catalog:withReadAccessDo(function()
|
|
1195
|
+
before = photo:getDevelopSettings()
|
|
1196
|
+
end)
|
|
1197
|
+
|
|
1198
|
+
if doTone then
|
|
1199
|
+
local ok, err = pcall(function()
|
|
1200
|
+
catalog:withWriteAccessDo("Auto Tone", function()
|
|
1201
|
+
LrDevelopController.setAutoTone()
|
|
1202
|
+
end)
|
|
1203
|
+
end)
|
|
1204
|
+
entry.applied.tone = ok
|
|
1205
|
+
if not ok then entry.tone_error = tostring(err) end
|
|
1206
|
+
end
|
|
1207
|
+
if doWB then
|
|
1208
|
+
local ok, err = pcall(function()
|
|
1209
|
+
catalog:withWriteAccessDo("Auto White Balance", function()
|
|
1210
|
+
LrDevelopController.setAutoWhiteBalance()
|
|
1211
|
+
end)
|
|
1212
|
+
end)
|
|
1213
|
+
entry.applied.white_balance = ok
|
|
1214
|
+
if not ok then entry.white_balance_error = tostring(err) end
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
-- The Auto commands commit through the Develop pipeline; give
|
|
1218
|
+
-- them a beat before reading back, then diff the main sliders so
|
|
1219
|
+
-- the caller sees exactly what Lightroom changed.
|
|
1220
|
+
LrTasks.sleep(AUTO_SETTLE_AFTER_COMMAND_S)
|
|
1221
|
+
local after = nil
|
|
1222
|
+
catalog:withReadAccessDo(function()
|
|
1223
|
+
after = photo:getDevelopSettings()
|
|
1224
|
+
end)
|
|
1225
|
+
|
|
1226
|
+
if type(before) == "table" and type(after) == "table" then
|
|
1227
|
+
local changed = {}
|
|
1228
|
+
for _, k in ipairs(AUTO_READBACK_KEYS) do
|
|
1229
|
+
if before[k] ~= after[k] then
|
|
1230
|
+
changed[k] = { before = before[k], after = after[k] }
|
|
1231
|
+
end
|
|
1232
|
+
end
|
|
1233
|
+
if next(changed) ~= nil then
|
|
1234
|
+
entry.changed = changed
|
|
1235
|
+
else
|
|
1236
|
+
entry.note = "Auto made no slider changes (settings may already be optimal, or the file type does not support Auto)"
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
if (not doTone or entry.applied.tone)
|
|
1241
|
+
and (not doWB or entry.applied.white_balance) then
|
|
1242
|
+
succeeded = succeeded + 1
|
|
1243
|
+
end
|
|
1244
|
+
end
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
Log.info(string.format("applyAuto(%s): %d/%d photos",
|
|
1248
|
+
table.concat(operations, "+"), succeeded, #photos))
|
|
1249
|
+
|
|
1250
|
+
local result = {
|
|
1251
|
+
success = succeeded == #photos,
|
|
1252
|
+
requested = #photos,
|
|
1253
|
+
succeeded = succeeded,
|
|
1254
|
+
failed = #photos - succeeded,
|
|
1255
|
+
operations = operations,
|
|
1256
|
+
results = results,
|
|
1257
|
+
method = "LrDevelopController.setAutoTone/setAutoWhiteBalance",
|
|
1258
|
+
message = string.format("Applied Auto (%s) to %d of %d photos",
|
|
1259
|
+
table.concat(operations, "+"), succeeded, #photos),
|
|
1260
|
+
}
|
|
1261
|
+
if succeeded < #photos then
|
|
1262
|
+
result.message = result.message
|
|
1263
|
+
.. ". Check each photo's 'error'/'tone_error'/'white_balance_error' entry."
|
|
1264
|
+
end
|
|
1265
|
+
return result
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
-- =====================================================================
|
|
1269
|
+
-- get_develop_settings — read the develop settings of a photo
|
|
1270
|
+
-- =====================================================================
|
|
1271
|
+
--
|
|
1272
|
+
-- The read-only counterpart of set_develop_settings: photo:getDevelopSettings()
|
|
1273
|
+
-- inside a read gate returns the full stored settings table. "basic" filters
|
|
1274
|
+
-- to the common develop sliders (plus curves) so the response stays small
|
|
1275
|
+
-- and LLM-friendly; "all" passes everything through cloneSerializable
|
|
1276
|
+
-- (depth-limited, handles the nested mask/retouch structures safely).
|
|
1277
|
+
-- Ported from lightroom-cli getSettings, which instead polls
|
|
1278
|
+
-- LrDevelopController per parameter; getDevelopSettings is one catalog read
|
|
1279
|
+
-- and works without switching modules.
|
|
1280
|
+
|
|
1281
|
+
local BASIC_DEVELOP_READ_KEYS = {
|
|
1282
|
+
"WhiteBalance", "Temperature", "Tint",
|
|
1283
|
+
"Exposure2012", "Contrast2012", "Highlights2012", "Shadows2012",
|
|
1284
|
+
"Whites2012", "Blacks2012", "Texture", "Clarity2012", "Dehaze",
|
|
1285
|
+
"Vibrance", "Saturation",
|
|
1286
|
+
"Sharpness", "LuminanceSmoothing", "ColorNoiseReduction",
|
|
1287
|
+
"ParametricShadows", "ParametricDarks", "ParametricLights",
|
|
1288
|
+
"ParametricHighlights",
|
|
1289
|
+
"ToneCurvePV2012", "ToneCurvePV2012Red", "ToneCurvePV2012Green",
|
|
1290
|
+
"ToneCurvePV2012Blue", "ToneCurveName2012",
|
|
1291
|
+
"ConvertToGrayscale", "PostCropVignetteAmount", "GrainAmount",
|
|
1292
|
+
"CropAngle", "ProcessVersion",
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function DevelopHandler.getDevelopSettings(args)
|
|
1296
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
1297
|
+
error("photo_id is required")
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
local fields = args.fields or "basic"
|
|
1301
|
+
if fields ~= "basic" and fields ~= "all" then
|
|
1302
|
+
error("fields must be 'basic' (default) or 'all'")
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
local catalog = LrApplication.activeCatalog()
|
|
1306
|
+
|
|
1307
|
+
local photo = nil
|
|
1308
|
+
catalog:withReadAccessDo(function()
|
|
1309
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1310
|
+
if resolved[1] then photo = resolved[1].photo end
|
|
1311
|
+
end)
|
|
1312
|
+
if not photo then
|
|
1313
|
+
error("No photo matched photo_id")
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
local settings = nil
|
|
1317
|
+
catalog:withReadAccessDo(function()
|
|
1318
|
+
settings = photo:getDevelopSettings()
|
|
1319
|
+
end)
|
|
1320
|
+
if type(settings) ~= "table" then
|
|
1321
|
+
error("getDevelopSettings returned no settings table")
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
local out = {}
|
|
1325
|
+
local skipped = {}
|
|
1326
|
+
local skippedCount = 0
|
|
1327
|
+
local function copySetting(key, value)
|
|
1328
|
+
local cloned, supported = cloneSerializable(value, 0, {})
|
|
1329
|
+
if supported then
|
|
1330
|
+
out[key] = cloned
|
|
1331
|
+
else
|
|
1332
|
+
skippedCount = skippedCount + 1
|
|
1333
|
+
table.insert(skipped, key)
|
|
1334
|
+
end
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
if fields == "all" then
|
|
1338
|
+
for key, value in pairs(settings) do
|
|
1339
|
+
copySetting(key, value)
|
|
1340
|
+
end
|
|
1341
|
+
else
|
|
1342
|
+
for _, key in ipairs(BASIC_DEVELOP_READ_KEYS) do
|
|
1343
|
+
if settings[key] ~= nil then
|
|
1344
|
+
copySetting(key, settings[key])
|
|
1345
|
+
end
|
|
1346
|
+
end
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
local count = 0
|
|
1350
|
+
for _ in pairs(out) do count = count + 1 end
|
|
1351
|
+
|
|
1352
|
+
Log.info(string.format("getDevelopSettings: %d field(s) (fields=%s, %d skipped)",
|
|
1353
|
+
count, fields, skippedCount))
|
|
1354
|
+
|
|
1355
|
+
local result = {
|
|
1356
|
+
success = true,
|
|
1357
|
+
photo = {
|
|
1358
|
+
id = photo.localIdentifier,
|
|
1359
|
+
path = photo:getRawMetadata('path'),
|
|
1360
|
+
filename = photo:getFormattedMetadata('fileName'),
|
|
1361
|
+
},
|
|
1362
|
+
fields = fields,
|
|
1363
|
+
settings = out,
|
|
1364
|
+
setting_count = count,
|
|
1365
|
+
message = string.format("Read %d develop setting(s)", count),
|
|
1366
|
+
}
|
|
1367
|
+
if skippedCount > 0 then
|
|
1368
|
+
result.skipped_fields = skipped
|
|
1369
|
+
result.message = result.message
|
|
1370
|
+
.. string.format(" (%d non-serializable field(s) skipped)", skippedCount)
|
|
1371
|
+
end
|
|
1372
|
+
return result
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
-- =====================================================================
|
|
1376
|
+
-- reset_develop — reset all adjustments, per tool, or per parameter
|
|
1377
|
+
-- =====================================================================
|
|
1378
|
+
--
|
|
1379
|
+
-- LrDevelopController exposes three reset surfaces (all reverse-engineered
|
|
1380
|
+
-- by lightroom-cli and verified against LrC 12/13):
|
|
1381
|
+
-- resetAllDevelopAdjustments() — everything, like the Reset button
|
|
1382
|
+
-- resetCrop()/resetTransforms()/... — per Develop tool
|
|
1383
|
+
-- resetToDefault(param) — one named parameter
|
|
1384
|
+
-- Like every LrDevelopController call they act on the photo loaded in the
|
|
1385
|
+
-- Develop module, so the handler switches modules and selects the photo
|
|
1386
|
+
-- first (outside any gate), then runs the resets inside write gates and
|
|
1387
|
+
-- verifies through a before/after settings diff.
|
|
1388
|
+
|
|
1389
|
+
local RESET_TOOLS = {
|
|
1390
|
+
crop = function() LrDevelopController.resetCrop() end,
|
|
1391
|
+
transforms = function() LrDevelopController.resetTransforms() end,
|
|
1392
|
+
spot_removal = function() LrDevelopController.resetSpotRemoval() end,
|
|
1393
|
+
redeye = function() LrDevelopController.resetRedeye() end,
|
|
1394
|
+
healing = function() LrDevelopController.resetHealing() end,
|
|
1395
|
+
masking = function() LrDevelopController.resetMasking() end,
|
|
1396
|
+
gradient = function() LrDevelopController.resetGradient() end,
|
|
1397
|
+
circular_gradient = function() LrDevelopController.resetCircularGradient() end,
|
|
1398
|
+
brushing = function() LrDevelopController.resetBrushing() end,
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
local function switchToDevelopAndSelectPhoto(catalog, photo)
|
|
1402
|
+
local switchOk, switchErr = pcall(function()
|
|
1403
|
+
LrApplicationView.switchToModule("develop")
|
|
1404
|
+
end)
|
|
1405
|
+
if not switchOk then
|
|
1406
|
+
error("could not switch Lightroom to the Develop module: " .. tostring(switchErr))
|
|
1407
|
+
end
|
|
1408
|
+
LrTasks.sleep(AUTO_SETTLE_AFTER_MODULE_SWITCH_S)
|
|
1409
|
+
|
|
1410
|
+
local selectOk = pcall(function()
|
|
1411
|
+
catalog:setSelectedPhotos(photo, { photo })
|
|
1412
|
+
end)
|
|
1413
|
+
if not selectOk then
|
|
1414
|
+
error("could not select the photo in Lightroom")
|
|
1415
|
+
end
|
|
1416
|
+
LrTasks.sleep(AUTO_SETTLE_BETWEEN_PHOTOS_S)
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
function DevelopHandler.resetDevelop(args)
|
|
1420
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
1421
|
+
error("photo_id is required")
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
local scope = args.scope or "all"
|
|
1425
|
+
if scope ~= "all" and scope ~= "tools" and scope ~= "params" then
|
|
1426
|
+
error("scope must be 'all' (default), 'tools' or 'params'")
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
local tools = args.tools
|
|
1430
|
+
local params = args.params
|
|
1431
|
+
|
|
1432
|
+
if scope == "tools" then
|
|
1433
|
+
if type(tools) ~= "table" or #tools == 0 then
|
|
1434
|
+
error("tools array is required when scope is 'tools' (crop, transforms, spot_removal, redeye, healing, masking, gradient, circular_gradient, brushing)")
|
|
1435
|
+
end
|
|
1436
|
+
for i, tool in ipairs(tools) do
|
|
1437
|
+
if not RESET_TOOLS[tool] then
|
|
1438
|
+
error("tools[" .. i .. "] '" .. tostring(tool) .. "' is not a resettable tool")
|
|
1439
|
+
end
|
|
1440
|
+
end
|
|
1441
|
+
elseif scope == "params" then
|
|
1442
|
+
if type(params) ~= "table" or #params == 0 then
|
|
1443
|
+
error("params array is required when scope is 'params'")
|
|
1444
|
+
end
|
|
1445
|
+
for i, key in ipairs(params) do
|
|
1446
|
+
requireAllowedDevelopSettingKey(key)
|
|
1447
|
+
end
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
local catalog = LrApplication.activeCatalog()
|
|
1451
|
+
|
|
1452
|
+
local photo = nil
|
|
1453
|
+
catalog:withReadAccessDo(function()
|
|
1454
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1455
|
+
if resolved[1] then photo = resolved[1].photo end
|
|
1456
|
+
end)
|
|
1457
|
+
if not photo then
|
|
1458
|
+
error("No photo matched photo_id")
|
|
1459
|
+
end
|
|
1460
|
+
|
|
1461
|
+
switchToDevelopAndSelectPhoto(catalog, photo)
|
|
1462
|
+
|
|
1463
|
+
local before = nil
|
|
1464
|
+
catalog:withReadAccessDo(function()
|
|
1465
|
+
before = photo:getDevelopSettings()
|
|
1466
|
+
end)
|
|
1467
|
+
|
|
1468
|
+
local applied = {}
|
|
1469
|
+
local errors = {}
|
|
1470
|
+
|
|
1471
|
+
if scope == "all" then
|
|
1472
|
+
local ok, err = pcall(function()
|
|
1473
|
+
catalog:withWriteAccessDo("Reset All Develop Adjustments", function()
|
|
1474
|
+
LrDevelopController.resetAllDevelopAdjustments()
|
|
1475
|
+
end)
|
|
1476
|
+
end)
|
|
1477
|
+
if ok then
|
|
1478
|
+
applied.all = true
|
|
1479
|
+
else
|
|
1480
|
+
errors.all = tostring(err)
|
|
1481
|
+
end
|
|
1482
|
+
elseif scope == "tools" then
|
|
1483
|
+
for _, tool in ipairs(tools) do
|
|
1484
|
+
local ok, err = pcall(function()
|
|
1485
|
+
catalog:withWriteAccessDo("Reset " .. tool, function()
|
|
1486
|
+
RESET_TOOLS[tool]()
|
|
1487
|
+
end)
|
|
1488
|
+
end)
|
|
1489
|
+
if ok then
|
|
1490
|
+
applied[tool] = true
|
|
1491
|
+
else
|
|
1492
|
+
errors[tool] = tostring(err)
|
|
1493
|
+
end
|
|
1494
|
+
end
|
|
1495
|
+
else
|
|
1496
|
+
for _, key in ipairs(params) do
|
|
1497
|
+
local ok, err = pcall(function()
|
|
1498
|
+
catalog:withWriteAccessDo("Reset Parameter", function()
|
|
1499
|
+
LrDevelopController.resetToDefault(key)
|
|
1500
|
+
end)
|
|
1501
|
+
end)
|
|
1502
|
+
if ok then
|
|
1503
|
+
applied[key] = true
|
|
1504
|
+
else
|
|
1505
|
+
errors[key] = tostring(err)
|
|
1506
|
+
end
|
|
1507
|
+
end
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
-- Give the Develop pipeline a beat, then diff the main sliders so the
|
|
1511
|
+
-- caller sees what actually changed (same read-back as applyAuto).
|
|
1512
|
+
LrTasks.sleep(AUTO_SETTLE_AFTER_COMMAND_S)
|
|
1513
|
+
local after = nil
|
|
1514
|
+
catalog:withReadAccessDo(function()
|
|
1515
|
+
after = photo:getDevelopSettings()
|
|
1516
|
+
end)
|
|
1517
|
+
|
|
1518
|
+
local changed = {}
|
|
1519
|
+
if type(before) == "table" and type(after) == "table" then
|
|
1520
|
+
for _, k in ipairs(BASIC_DEVELOP_READ_KEYS) do
|
|
1521
|
+
if before[k] ~= nil and before[k] ~= after[k] then
|
|
1522
|
+
changed[k] = { before = before[k], after = after[k] }
|
|
1523
|
+
end
|
|
1524
|
+
end
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
local failedCount = 0
|
|
1528
|
+
for _ in pairs(errors) do failedCount = failedCount + 1 end
|
|
1529
|
+
|
|
1530
|
+
Log.info(string.format("resetDevelop(%s): %d reset(s), %d error(s)",
|
|
1531
|
+
scope, (scope == "all" and 1 or (scope == "tools" and #tools or #params)), failedCount))
|
|
1532
|
+
|
|
1533
|
+
local result = {
|
|
1534
|
+
success = failedCount == 0,
|
|
1535
|
+
scope = scope,
|
|
1536
|
+
applied = applied,
|
|
1537
|
+
errors = (next(errors) ~= nil) and errors or nil,
|
|
1538
|
+
changed = (next(changed) ~= nil) and changed or nil,
|
|
1539
|
+
message = string.format("Reset develop settings (scope=%s)%s",
|
|
1540
|
+
scope, failedCount > 0 and (" — " .. failedCount .. " error(s)") or ""),
|
|
1541
|
+
}
|
|
1542
|
+
if next(changed) == nil and failedCount == 0 then
|
|
1543
|
+
result.note = "No slider changed — the settings may already be at defaults."
|
|
1544
|
+
end
|
|
1545
|
+
return result
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
-- =====================================================================
|
|
1549
|
+
-- set_process_version — process/engine version of the loaded photo
|
|
1550
|
+
-- =====================================================================
|
|
1551
|
+
--
|
|
1552
|
+
-- LrDevelopController.setProcessVersion("Version N") switches the process
|
|
1553
|
+
-- (calibration engine) of the photo loaded in Develop: Version 3 is
|
|
1554
|
+
-- Process 2012, higher numbers are newer engines (LrC 13+ = Version 6).
|
|
1555
|
+
-- Older photos can be modernized (then AI masking/Denoise unlock) at the
|
|
1556
|
+
-- cost of a rendering change. Read-back via getProcessVersion confirms.
|
|
1557
|
+
|
|
1558
|
+
function DevelopHandler.setProcessVersion(args)
|
|
1559
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
1560
|
+
error("photo_id is required")
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
local version = args.version
|
|
1564
|
+
if version == nil then
|
|
1565
|
+
error("version is required (e.g. 'Version 3' = Process 2012, 'Version 6' = newest)")
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
local catalog = LrApplication.activeCatalog()
|
|
1569
|
+
|
|
1570
|
+
local photo = nil
|
|
1571
|
+
catalog:withReadAccessDo(function()
|
|
1572
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1573
|
+
if resolved[1] then photo = resolved[1].photo end
|
|
1574
|
+
end)
|
|
1575
|
+
if not photo then
|
|
1576
|
+
error("No photo matched photo_id")
|
|
1577
|
+
end
|
|
1578
|
+
|
|
1579
|
+
switchToDevelopAndSelectPhoto(catalog, photo)
|
|
1580
|
+
|
|
1581
|
+
local beforeVersion = nil
|
|
1582
|
+
pcall(function() beforeVersion = LrDevelopController.getProcessVersion() end)
|
|
1583
|
+
|
|
1584
|
+
local ok, err = pcall(function()
|
|
1585
|
+
catalog:withWriteAccessDo("Set Process Version", function()
|
|
1586
|
+
LrDevelopController.setProcessVersion(version)
|
|
1587
|
+
end)
|
|
1588
|
+
end)
|
|
1589
|
+
if not ok then
|
|
1590
|
+
error("setProcessVersion failed: " .. tostring(err)
|
|
1591
|
+
.. " (the photo may already be on this engine, or the LrC version does not accept it)")
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
LrTasks.sleep(AUTO_SETTLE_AFTER_COMMAND_S)
|
|
1595
|
+
local afterVersion = nil
|
|
1596
|
+
pcall(function() afterVersion = LrDevelopController.getProcessVersion() end)
|
|
1597
|
+
|
|
1598
|
+
Log.info(string.format("setProcessVersion: %s -> %s (requested %s)",
|
|
1599
|
+
tostring(beforeVersion), tostring(afterVersion), version))
|
|
1600
|
+
|
|
1601
|
+
local result = {
|
|
1602
|
+
success = true,
|
|
1603
|
+
version = version,
|
|
1604
|
+
before = beforeVersion,
|
|
1605
|
+
after = afterVersion,
|
|
1606
|
+
verified = (afterVersion ~= nil and afterVersion == version),
|
|
1607
|
+
message = string.format("Process version: %s -> %s",
|
|
1608
|
+
tostring(beforeVersion), tostring(afterVersion or version)),
|
|
1609
|
+
}
|
|
1610
|
+
if afterVersion ~= nil and afterVersion ~= version then
|
|
1611
|
+
result.warning = "Lightroom reports a different process version than requested: "
|
|
1612
|
+
.. tostring(afterVersion)
|
|
1613
|
+
end
|
|
1614
|
+
return result
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
-- =====================================================================
|
|
1618
|
+
-- create_snapshot — named develop snapshot (undo checkpoint)
|
|
1619
|
+
-- =====================================================================
|
|
1620
|
+
--
|
|
1621
|
+
-- photo:createDevelopSnapshot(name) records the current develop state as
|
|
1622
|
+
-- a Snapshots panel entry. The SDK exposes no list/apply/delete for
|
|
1623
|
+
-- snapshots, so this is create-only — reported honestly instead of
|
|
1624
|
+
-- pretending a read-back. Ported from lightroom-cli createDevelopSnapshot.
|
|
1625
|
+
|
|
1626
|
+
function DevelopHandler.createSnapshot(args)
|
|
1627
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
1628
|
+
error("photo_id is required")
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
local name = args.name
|
|
1632
|
+
if type(name) ~= "string" or name == "" then
|
|
1633
|
+
error("name is required")
|
|
1634
|
+
end
|
|
1635
|
+
if #name > 255 then
|
|
1636
|
+
error("name must be at most 255 characters")
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
local catalog = LrApplication.activeCatalog()
|
|
1640
|
+
|
|
1641
|
+
local photo = nil
|
|
1642
|
+
catalog:withReadAccessDo(function()
|
|
1643
|
+
local resolved = PhotoLookup.resolveMany(catalog, { args.photo_id })
|
|
1644
|
+
if resolved[1] then photo = resolved[1].photo end
|
|
1645
|
+
end)
|
|
1646
|
+
if not photo then
|
|
1647
|
+
error("No photo matched photo_id")
|
|
1648
|
+
end
|
|
1649
|
+
|
|
1650
|
+
catalog:withWriteAccessDo("Create Develop Snapshot", function()
|
|
1651
|
+
photo:createDevelopSnapshot(name)
|
|
1652
|
+
end)
|
|
1653
|
+
|
|
1654
|
+
Log.info(string.format("createSnapshot('%s') on photo %s", name, tostring(photo.localIdentifier)))
|
|
1655
|
+
|
|
1656
|
+
return {
|
|
1657
|
+
success = true,
|
|
1658
|
+
photo = {
|
|
1659
|
+
id = photo.localIdentifier,
|
|
1660
|
+
path = photo:getRawMetadata('path'),
|
|
1661
|
+
},
|
|
1662
|
+
name = name,
|
|
1663
|
+
message = string.format("Created develop snapshot '%s'", name),
|
|
1664
|
+
note = "The SDK cannot list snapshots back; verify visually in Lightroom's Snapshots panel.",
|
|
1665
|
+
}
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
return DevelopHandler
|