@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,549 @@
|
|
|
1
|
+
local LrApplication = import 'LrApplication'
|
|
2
|
+
local LrTasks = import 'LrTasks'
|
|
3
|
+
local LrApplicationView = import 'LrApplicationView'
|
|
4
|
+
local LrDevelopController = import 'LrDevelopController'
|
|
5
|
+
|
|
6
|
+
local PhotoLookup = require 'PhotoLookup'
|
|
7
|
+
local Log = require 'Log'
|
|
8
|
+
|
|
9
|
+
local AIMaskHandler = {}
|
|
10
|
+
|
|
11
|
+
-- =====================================================================
|
|
12
|
+
-- add_ai_mask — AI subject/sky/background selection masks
|
|
13
|
+
-- =====================================================================
|
|
14
|
+
--
|
|
15
|
+
-- Lightroom Classic's AI masking (Select Subject / Select Sky / ...) is
|
|
16
|
+
-- exposed to the SDK through LrDevelopController:
|
|
17
|
+
--
|
|
18
|
+
-- maskId = LrDevelopController.createNewMask("aiSelection", subtype)
|
|
19
|
+
--
|
|
20
|
+
-- Unlike the catalog write APIs used by most handlers, LrDevelopController
|
|
21
|
+
-- drives the Develop module of the running Lightroom UI: the target photo
|
|
22
|
+
-- must be selected in the filmstrip and the Develop module active, and the
|
|
23
|
+
-- AI computation itself runs inside Lightroom (seconds on fast machines).
|
|
24
|
+
-- The handler therefore, per photo:
|
|
25
|
+
--
|
|
26
|
+
-- 1. selects the photo (outside any catalog gate — it yields, #134/#124);
|
|
27
|
+
-- 2. switches to the Develop module (once, before the batch);
|
|
28
|
+
-- 3. engages the masking tool if another tool is active;
|
|
29
|
+
-- 4. creates the AI selection mask;
|
|
30
|
+
-- 5. applies the requested adjustment sliders to the new mask.
|
|
31
|
+
--
|
|
32
|
+
-- Everything is pcall-wrapped and reported honestly per photo: if a
|
|
33
|
+
-- particular subtype is unavailable in the installed Lightroom version,
|
|
34
|
+
-- or the photo has no detectable subject, the error surfaces in that
|
|
35
|
+
-- photo's entry instead of failing the whole call.
|
|
36
|
+
--
|
|
37
|
+
-- Best verified visually afterwards with get_photo_preview.
|
|
38
|
+
|
|
39
|
+
-- Subtypes accepted by createNewMask("aiSelection", ...). Availability of
|
|
40
|
+
-- people/objects/landscape depends on the Lightroom Classic version.
|
|
41
|
+
local SELECTION_TYPES = {
|
|
42
|
+
subject = true,
|
|
43
|
+
sky = true,
|
|
44
|
+
background = true,
|
|
45
|
+
objects = true,
|
|
46
|
+
people = true,
|
|
47
|
+
landscape = true,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
-- Friendly arg name -> LrDevelopController setValue param + valid range.
|
|
51
|
+
-- Temperature/Tint are deliberately NOT offered here: in the masking
|
|
52
|
+
-- context those sliders are relative (-100..100) while the global Develop
|
|
53
|
+
-- parameters are Kelvin/-150..150, and the SDK docs do not pin down which
|
|
54
|
+
-- one setValue targets while a mask is active. Rather than risk silently
|
|
55
|
+
-- overwriting the global white balance, local color work goes through
|
|
56
|
+
-- add_local_adjustment (LocalTemperature2012/LocalTint2012, verified).
|
|
57
|
+
local ADJUSTMENTS = {
|
|
58
|
+
exposure = { key = "Exposure2012", min = -5, max = 5 },
|
|
59
|
+
contrast = { key = "Contrast2012", min = -100, max = 100 },
|
|
60
|
+
highlights = { key = "Highlights2012", min = -100, max = 100 },
|
|
61
|
+
shadows = { key = "Shadows2012", min = -100, max = 100 },
|
|
62
|
+
whites = { key = "Whites2012", min = -100, max = 100 },
|
|
63
|
+
blacks = { key = "Blacks2012", min = -100, max = 100 },
|
|
64
|
+
texture = { key = "Texture", min = -100, max = 100 },
|
|
65
|
+
clarity = { key = "Clarity2012", min = -100, max = 100 },
|
|
66
|
+
dehaze = { key = "Dehaze", min = -100, max = 100 },
|
|
67
|
+
vibrance = { key = "Vibrance", min = -100, max = 100 },
|
|
68
|
+
saturation = { key = "Saturation", min = -100, max = 100 },
|
|
69
|
+
sharpness = { key = "Sharpness", min = 0, max = 150 },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
local SUPPORTED_ADJUSTMENT_NAMES = (function()
|
|
73
|
+
local names = {}
|
|
74
|
+
for k in pairs(ADJUSTMENTS) do table.insert(names, k) end
|
|
75
|
+
table.sort(names)
|
|
76
|
+
return table.concat(names, ", ")
|
|
77
|
+
end)()
|
|
78
|
+
|
|
79
|
+
-- Named adjustment recipes for AI/range masks (ported from lightroom-cli's
|
|
80
|
+
-- presets.py, minus 'warm-skin': it relies on Temp/Tint, which this handler
|
|
81
|
+
-- deliberately does not expose — see the ADJUSTMENTS note above).
|
|
82
|
+
local ADJUSTMENT_PRESETS = {
|
|
83
|
+
darken_sky = { exposure = -0.7, highlights = -30, saturation = 15 },
|
|
84
|
+
brighten_subject = { exposure = 0.5, shadows = 20, clarity = 10 },
|
|
85
|
+
blur_background = { sharpness = -80, clarity = -40 },
|
|
86
|
+
enhance_landscape = { clarity = 30, vibrance = 25, dehaze = 15 },
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
local SETTLE_AFTER_MODULE_SWITCH_S = 0.5
|
|
90
|
+
local SETTLE_BETWEEN_PHOTOS_S = 0.3
|
|
91
|
+
|
|
92
|
+
local function buildAdjustments(input)
|
|
93
|
+
if input == nil then return {} end
|
|
94
|
+
if type(input) ~= "table" then
|
|
95
|
+
error("adjustments must be an object")
|
|
96
|
+
end
|
|
97
|
+
local out = {}
|
|
98
|
+
for name, value in pairs(input) do
|
|
99
|
+
local spec = ADJUSTMENTS[name]
|
|
100
|
+
if not spec then
|
|
101
|
+
error("unknown adjustment '" .. tostring(name)
|
|
102
|
+
.. "' (supported: " .. SUPPORTED_ADJUSTMENT_NAMES .. ")")
|
|
103
|
+
end
|
|
104
|
+
if type(value) ~= "number" then
|
|
105
|
+
error("adjustment '" .. tostring(name) .. "' must be a number")
|
|
106
|
+
end
|
|
107
|
+
if value < spec.min or value > spec.max then
|
|
108
|
+
error(string.format("adjustment '%s' must be between %s and %s",
|
|
109
|
+
name, tostring(spec.min), tostring(spec.max)))
|
|
110
|
+
end
|
|
111
|
+
table.insert(out, { name = name, key = spec.key, value = value })
|
|
112
|
+
end
|
|
113
|
+
return out
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
local function resolveAdjustments(args)
|
|
117
|
+
if args.adjustments ~= nil and args.adjustment_preset ~= nil then
|
|
118
|
+
error("pass either adjustments or adjustment_preset, not both")
|
|
119
|
+
end
|
|
120
|
+
if args.adjustment_preset ~= nil then
|
|
121
|
+
local preset = ADJUSTMENT_PRESETS[args.adjustment_preset]
|
|
122
|
+
if not preset then
|
|
123
|
+
error("adjustment_preset must be one of: darken_sky, brighten_subject, blur_background, enhance_landscape")
|
|
124
|
+
end
|
|
125
|
+
return buildAdjustments(preset), args.adjustment_preset
|
|
126
|
+
end
|
|
127
|
+
return buildAdjustments(args.adjustments), nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
-- Shared per-photo batch loop: selects each photo in Develop, engages the
|
|
131
|
+
-- masking tool if needed, creates the mask through `createMask()`, then
|
|
132
|
+
-- applies the requested adjustment sliders. Returns (results, succeeded).
|
|
133
|
+
local function runMaskCreationBatch(catalog, photos, createMask, adjustments)
|
|
134
|
+
local results = {}
|
|
135
|
+
local succeeded = 0
|
|
136
|
+
local failed = 0
|
|
137
|
+
|
|
138
|
+
for i, photo in ipairs(photos) do
|
|
139
|
+
if i > 1 then LrTasks.sleep(SETTLE_BETWEEN_PHOTOS_S) end
|
|
140
|
+
|
|
141
|
+
local entry = {
|
|
142
|
+
photo = {
|
|
143
|
+
id = photo.localIdentifier,
|
|
144
|
+
path = photo:getRawMetadata('path'),
|
|
145
|
+
filename = photo:getFormattedMetadata('fileName'),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
table.insert(results, entry)
|
|
149
|
+
|
|
150
|
+
local ok = pcall(function()
|
|
151
|
+
catalog:setSelectedPhotos(photo, { photo })
|
|
152
|
+
end)
|
|
153
|
+
if not ok then
|
|
154
|
+
entry.error = "could not select the photo in Lightroom"
|
|
155
|
+
failed = failed + 1
|
|
156
|
+
else
|
|
157
|
+
-- Engage the masking tool only if another tool is active; a
|
|
158
|
+
-- fresh createNewMask usually does this itself, but the guard
|
|
159
|
+
-- mirrors how the Develop UI behaves when a tool is stuck open.
|
|
160
|
+
local currentToolOk, currentTool = pcall(function()
|
|
161
|
+
return LrDevelopController.getSelectedTool()
|
|
162
|
+
end)
|
|
163
|
+
if currentToolOk and currentTool ~= "masking" then
|
|
164
|
+
pcall(function() LrDevelopController.selectTool("masking") end)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
local maskOk, maskId = pcall(createMask)
|
|
168
|
+
if not maskOk or maskId == nil then
|
|
169
|
+
entry.error = "createNewMask failed: " .. tostring(maskId)
|
|
170
|
+
failed = failed + 1
|
|
171
|
+
else
|
|
172
|
+
entry.mask_id = maskId
|
|
173
|
+
entry.applied = {}
|
|
174
|
+
entry.adjustment_errors = {}
|
|
175
|
+
|
|
176
|
+
for _, adj in ipairs(adjustments) do
|
|
177
|
+
local applied, err = pcall(function()
|
|
178
|
+
LrDevelopController.setValue(adj.key, adj.value)
|
|
179
|
+
end)
|
|
180
|
+
if applied then
|
|
181
|
+
entry.applied[adj.name] = adj.value
|
|
182
|
+
else
|
|
183
|
+
entry.adjustment_errors[adj.name] = tostring(err)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
if next(entry.adjustment_errors) == nil then
|
|
187
|
+
entry.adjustment_errors = nil
|
|
188
|
+
end
|
|
189
|
+
succeeded = succeeded + 1
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return results, succeeded, failed
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
local function resolveSinglePhoto(catalog, photoId)
|
|
198
|
+
local photo = nil
|
|
199
|
+
catalog:withReadAccessDo(function()
|
|
200
|
+
local resolved = PhotoLookup.resolveMany(catalog, { photoId })
|
|
201
|
+
if resolved[1] and resolved[1].photo then
|
|
202
|
+
photo = resolved[1].photo
|
|
203
|
+
end
|
|
204
|
+
end)
|
|
205
|
+
return photo
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
local function switchToDevelopAndSelect(catalog, photo)
|
|
209
|
+
-- switchToModule and setSelectedPhotos both yield to the UI thread, so
|
|
210
|
+
-- they must run OUTSIDE any catalog access gate (#134/#124).
|
|
211
|
+
local switchOk, switchErr = pcall(function()
|
|
212
|
+
LrApplicationView.switchToModule("develop")
|
|
213
|
+
end)
|
|
214
|
+
if not switchOk then
|
|
215
|
+
error("could not switch Lightroom to the Develop module: "
|
|
216
|
+
.. tostring(switchErr))
|
|
217
|
+
end
|
|
218
|
+
LrTasks.sleep(SETTLE_AFTER_MODULE_SWITCH_S)
|
|
219
|
+
|
|
220
|
+
local selectOk = pcall(function()
|
|
221
|
+
catalog:setSelectedPhotos(photo, { photo })
|
|
222
|
+
end)
|
|
223
|
+
if not selectOk then
|
|
224
|
+
error("could not select the photo in Lightroom")
|
|
225
|
+
end
|
|
226
|
+
LrTasks.sleep(SETTLE_BETWEEN_PHOTOS_S)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
function AIMaskHandler.addAIMask(args)
|
|
230
|
+
args = args or {}
|
|
231
|
+
if not args.photo_ids or #args.photo_ids == 0 then
|
|
232
|
+
error("photo_ids is required")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
local selectionType = args.selection_type or "subject"
|
|
236
|
+
if not SELECTION_TYPES[selectionType] then
|
|
237
|
+
error("selection_type must be one of: subject, sky, background, objects, people, landscape")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
local adjustments, presetName = resolveAdjustments(args)
|
|
241
|
+
|
|
242
|
+
local catalog = LrApplication.activeCatalog()
|
|
243
|
+
|
|
244
|
+
local photos = {}
|
|
245
|
+
catalog:withReadAccessDo(function()
|
|
246
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
|
|
247
|
+
for _, entry in ipairs(resolved) do
|
|
248
|
+
if entry.photo then table.insert(photos, entry.photo) end
|
|
249
|
+
end
|
|
250
|
+
end)
|
|
251
|
+
|
|
252
|
+
if #photos == 0 then
|
|
253
|
+
error("No photos matched photo_ids")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
-- switchToModule and setSelectedPhotos both yield to the UI thread, so
|
|
257
|
+
-- they must run OUTSIDE any catalog access gate (#134/#124).
|
|
258
|
+
local switchOk, switchErr = pcall(function()
|
|
259
|
+
LrApplicationView.switchToModule("develop")
|
|
260
|
+
end)
|
|
261
|
+
if not switchOk then
|
|
262
|
+
error("could not switch Lightroom to the Develop module: "
|
|
263
|
+
.. tostring(switchErr))
|
|
264
|
+
end
|
|
265
|
+
LrTasks.sleep(SETTLE_AFTER_MODULE_SWITCH_S)
|
|
266
|
+
|
|
267
|
+
local results, succeeded, failed = runMaskCreationBatch(catalog, photos, function()
|
|
268
|
+
return LrDevelopController.createNewMask("aiSelection", selectionType)
|
|
269
|
+
end, adjustments)
|
|
270
|
+
|
|
271
|
+
Log.info(string.format("addAIMask('%s'): %d/%d photos",
|
|
272
|
+
selectionType, succeeded, #photos))
|
|
273
|
+
|
|
274
|
+
local result = {
|
|
275
|
+
success = failed == 0,
|
|
276
|
+
selection_type = selectionType,
|
|
277
|
+
requested = #photos,
|
|
278
|
+
succeeded = succeeded,
|
|
279
|
+
failed = failed,
|
|
280
|
+
results = results,
|
|
281
|
+
method = "LrDevelopController.createNewMask('aiSelection')",
|
|
282
|
+
message = string.format("AI '%s' mask applied to %d of %d photos",
|
|
283
|
+
selectionType, succeeded, #photos),
|
|
284
|
+
}
|
|
285
|
+
if presetName then
|
|
286
|
+
result.adjustment_preset = presetName
|
|
287
|
+
end
|
|
288
|
+
if failed > 0 then
|
|
289
|
+
result.message = result.message
|
|
290
|
+
.. ". Check each photo's 'error' entry; verify masks visually with get_photo_preview."
|
|
291
|
+
end
|
|
292
|
+
return result
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
-- =====================================================================
|
|
296
|
+
-- add_range_mask — luminance / color / depth range masks
|
|
297
|
+
-- =====================================================================
|
|
298
|
+
--
|
|
299
|
+
-- LrDevelopController.createNewMask("rangeMask", "luminance"|"color"|"depth")
|
|
300
|
+
-- creates a range mask on the photo loaded in Develop, then setValue
|
|
301
|
+
-- applies the adjustment sliders to it (same flow as the AI masks).
|
|
302
|
+
-- HONEST LIMITATION (also present in lightroom-cli): the SDK cannot set
|
|
303
|
+
-- the range bounds or sample points, so a fresh range mask covers the
|
|
304
|
+
-- full range — refine the bounds in the Lightroom UI afterwards, or use
|
|
305
|
+
-- the adjustment sliders from here. 'depth' requires a photo that has
|
|
306
|
+
-- depth data (iPhone portrait etc.).
|
|
307
|
+
|
|
308
|
+
local RANGE_TYPES = {
|
|
309
|
+
luminance = true,
|
|
310
|
+
color = true,
|
|
311
|
+
depth = true,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function AIMaskHandler.addRangeMask(args)
|
|
315
|
+
args = args or {}
|
|
316
|
+
if not args.photo_ids or #args.photo_ids == 0 then
|
|
317
|
+
error("photo_ids is required")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
local rangeType = args.range_type or "luminance"
|
|
321
|
+
if not RANGE_TYPES[rangeType] then
|
|
322
|
+
error("range_type must be one of: luminance, color, depth")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
local adjustments, presetName = resolveAdjustments(args)
|
|
326
|
+
|
|
327
|
+
local catalog = LrApplication.activeCatalog()
|
|
328
|
+
|
|
329
|
+
local photos = {}
|
|
330
|
+
catalog:withReadAccessDo(function()
|
|
331
|
+
local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
|
|
332
|
+
for _, entry in ipairs(resolved) do
|
|
333
|
+
if entry.photo then table.insert(photos, entry.photo) end
|
|
334
|
+
end
|
|
335
|
+
end)
|
|
336
|
+
|
|
337
|
+
if #photos == 0 then
|
|
338
|
+
error("No photos matched photo_ids")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
local switchOk, switchErr = pcall(function()
|
|
342
|
+
LrApplicationView.switchToModule("develop")
|
|
343
|
+
end)
|
|
344
|
+
if not switchOk then
|
|
345
|
+
error("could not switch Lightroom to the Develop module: "
|
|
346
|
+
.. tostring(switchErr))
|
|
347
|
+
end
|
|
348
|
+
LrTasks.sleep(SETTLE_AFTER_MODULE_SWITCH_S)
|
|
349
|
+
|
|
350
|
+
local results, succeeded, failed = runMaskCreationBatch(catalog, photos, function()
|
|
351
|
+
return LrDevelopController.createNewMask("rangeMask", rangeType)
|
|
352
|
+
end, adjustments)
|
|
353
|
+
|
|
354
|
+
Log.info(string.format("addRangeMask('%s'): %d/%d photos",
|
|
355
|
+
rangeType, succeeded, #photos))
|
|
356
|
+
|
|
357
|
+
local result = {
|
|
358
|
+
success = failed == 0,
|
|
359
|
+
range_type = rangeType,
|
|
360
|
+
requested = #photos,
|
|
361
|
+
succeeded = succeeded,
|
|
362
|
+
failed = failed,
|
|
363
|
+
results = results,
|
|
364
|
+
method = "LrDevelopController.createNewMask('rangeMask')",
|
|
365
|
+
message = string.format("Range '%s' mask applied to %d of %d photos",
|
|
366
|
+
rangeType, succeeded, #photos),
|
|
367
|
+
note = "The SDK cannot set range bounds or sample points: the mask "
|
|
368
|
+
.. "covers the full range. Refine the bounds in Lightroom's UI, or "
|
|
369
|
+
.. "drive the look through the adjustment sliders.",
|
|
370
|
+
}
|
|
371
|
+
if presetName then
|
|
372
|
+
result.adjustment_preset = presetName
|
|
373
|
+
end
|
|
374
|
+
if failed > 0 then
|
|
375
|
+
result.message = result.message
|
|
376
|
+
.. ". Check each photo's 'error' entry."
|
|
377
|
+
end
|
|
378
|
+
return result
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
-- =====================================================================
|
|
382
|
+
-- toggle_mask_overlay — show/hide mask overlays in the Develop UI
|
|
383
|
+
-- =====================================================================
|
|
384
|
+
--
|
|
385
|
+
-- LrDevelopController.toggleOverlay() flips the red mask overlay in the
|
|
386
|
+
-- Develop module, the fastest way for a human to eyeball what add_ai_mask /
|
|
387
|
+
-- add_local_adjustment actually selected. Acts on the photo loaded in
|
|
388
|
+
-- Develop; this handler selects the requested photo first so the overlay
|
|
389
|
+
-- belongs to it deterministically.
|
|
390
|
+
|
|
391
|
+
function AIMaskHandler.toggleMaskOverlay(args)
|
|
392
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
393
|
+
error("photo_id is required")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
local catalog = LrApplication.activeCatalog()
|
|
397
|
+
local photo = resolveSinglePhoto(catalog, args.photo_id)
|
|
398
|
+
if not photo then
|
|
399
|
+
error("No photo matched photo_id")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
switchToDevelopAndSelect(catalog, photo)
|
|
403
|
+
|
|
404
|
+
local ok, err = pcall(function()
|
|
405
|
+
LrDevelopController.toggleOverlay()
|
|
406
|
+
end)
|
|
407
|
+
if not ok then
|
|
408
|
+
error("toggleOverlay failed: " .. tostring(err)
|
|
409
|
+
.. " (requires the Develop module to be showing this photo)")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
Log.info(string.format("toggleMaskOverlay: photo %s", tostring(photo.localIdentifier)))
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
success = true,
|
|
416
|
+
photo = {
|
|
417
|
+
id = photo.localIdentifier,
|
|
418
|
+
path = photo:getRawMetadata('path'),
|
|
419
|
+
},
|
|
420
|
+
message = "Mask overlay toggled — check the Lightroom Develop view "
|
|
421
|
+
.. "(red overlay shows what is masked).",
|
|
422
|
+
}
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
-- =====================================================================
|
|
426
|
+
-- list_masks / remove_mask — mask inventory management
|
|
427
|
+
-- =====================================================================
|
|
428
|
+
--
|
|
429
|
+
-- LrDevelopController.getAllMasks() returns the masks of the photo loaded
|
|
430
|
+
-- in the Develop module. The exact per-entry shape varies between Lightroom
|
|
431
|
+
-- versions (name/id fields), so entries are passed through verbatim — pair
|
|
432
|
+
-- list_masks with read_local_adjustments for the full stored structure.
|
|
433
|
+
-- removeMask verifies through the mask count before/after, which is stable
|
|
434
|
+
-- regardless of entry shape.
|
|
435
|
+
|
|
436
|
+
local function currentMaskCount()
|
|
437
|
+
local ok, masks = pcall(function()
|
|
438
|
+
return LrDevelopController.getAllMasks()
|
|
439
|
+
end)
|
|
440
|
+
if not ok or type(masks) ~= "table" then return -1 end
|
|
441
|
+
return #masks
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
function AIMaskHandler.listMasks(args)
|
|
445
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
446
|
+
error("photo_id is required")
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
local catalog = LrApplication.activeCatalog()
|
|
450
|
+
local photo = resolveSinglePhoto(catalog, args.photo_id)
|
|
451
|
+
if not photo then
|
|
452
|
+
error("No photo matched photo_id")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
switchToDevelopAndSelect(catalog, photo)
|
|
456
|
+
|
|
457
|
+
local masksOk, masks = pcall(function()
|
|
458
|
+
return LrDevelopController.getAllMasks()
|
|
459
|
+
end)
|
|
460
|
+
if not masksOk then
|
|
461
|
+
error("getAllMasks() failed: " .. tostring(masks))
|
|
462
|
+
end
|
|
463
|
+
masks = masks or {}
|
|
464
|
+
|
|
465
|
+
Log.info(string.format("listMasks: %d mask(s)", #masks))
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
success = true,
|
|
469
|
+
photo = {
|
|
470
|
+
id = photo.localIdentifier,
|
|
471
|
+
path = photo:getRawMetadata('path'),
|
|
472
|
+
},
|
|
473
|
+
masks = masks,
|
|
474
|
+
count = #masks,
|
|
475
|
+
message = string.format("Photo has %d mask(s)", #masks),
|
|
476
|
+
}
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
function AIMaskHandler.removeMask(args)
|
|
480
|
+
if args.photo_id == nil or args.photo_id == "" then
|
|
481
|
+
error("photo_id is required")
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
local removeAll = args.remove_all == true
|
|
485
|
+
|
|
486
|
+
if not removeAll and args.mask_id == nil then
|
|
487
|
+
error("mask_id is required (or pass remove_all=true to clear every mask)")
|
|
488
|
+
end
|
|
489
|
+
if removeAll and args.confirm ~= true then
|
|
490
|
+
error("remove_all strips every mask from the photo: pass confirm=true to proceed")
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
local catalog = LrApplication.activeCatalog()
|
|
494
|
+
local photo = resolveSinglePhoto(catalog, args.photo_id)
|
|
495
|
+
if not photo then
|
|
496
|
+
error("No photo matched photo_id")
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
switchToDevelopAndSelect(catalog, photo)
|
|
500
|
+
|
|
501
|
+
local beforeCount = currentMaskCount()
|
|
502
|
+
|
|
503
|
+
if removeAll then
|
|
504
|
+
-- resetMasking() is the engine's "remove all masks" command
|
|
505
|
+
-- (lightroom-cli's `ai reset`).
|
|
506
|
+
local resetOk, resetErr = pcall(function()
|
|
507
|
+
LrDevelopController.resetMasking()
|
|
508
|
+
end)
|
|
509
|
+
if not resetOk then
|
|
510
|
+
error("resetMasking failed: " .. tostring(resetErr))
|
|
511
|
+
end
|
|
512
|
+
else
|
|
513
|
+
local delOk, delErr = pcall(function()
|
|
514
|
+
LrDevelopController.deleteMask(args.mask_id)
|
|
515
|
+
end)
|
|
516
|
+
if not delOk then
|
|
517
|
+
error("deleteMask failed: " .. tostring(delErr)
|
|
518
|
+
.. " (the mask id must belong to the current photo — check list_masks)")
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
-- Give the Develop module a beat to commit the deletion, then verify
|
|
523
|
+
-- through the mask count: the per-entry shape of getAllMasks() varies
|
|
524
|
+
-- between Lightroom versions, but a decrease is unambiguous.
|
|
525
|
+
LrTasks.sleep(SETTLE_BETWEEN_PHOTOS_S)
|
|
526
|
+
local afterCount = currentMaskCount()
|
|
527
|
+
local verified = beforeCount >= 0 and afterCount >= 0 and afterCount < beforeCount
|
|
528
|
+
|
|
529
|
+
Log.info(string.format("removeMask(%s): %d -> %d masks",
|
|
530
|
+
removeAll and "all" or tostring(args.mask_id), beforeCount, afterCount))
|
|
531
|
+
|
|
532
|
+
local result = {
|
|
533
|
+
success = true,
|
|
534
|
+
remove_all = removeAll,
|
|
535
|
+
masks_before = beforeCount,
|
|
536
|
+
masks_after = afterCount,
|
|
537
|
+
verified = verified,
|
|
538
|
+
message = string.format("%s (masks %d -> %d)",
|
|
539
|
+
removeAll and "Removed all masks" or ("Removed mask '" .. tostring(args.mask_id) .. "'"),
|
|
540
|
+
beforeCount, afterCount),
|
|
541
|
+
}
|
|
542
|
+
if not verified then
|
|
543
|
+
result.warning = "Mask count did not decrease — the photo may have "
|
|
544
|
+
.. "had no masks, or the id did not match. Run list_masks to inspect."
|
|
545
|
+
end
|
|
546
|
+
return result
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
return AIMaskHandler
|