Writing Lightroom Classic Plugins
Getting Started¶
In this article, we will explore how to write Lightroom Classic plugins using Lua, a lightweight scripting language designed for embedded systems and games.
To get started, you'll want to download the Lightroom Classic SDK, which Adobe makes a huge pain in the ass to procure.
To find it, first go here and search for "lightroom":
Clicking "View downloads" brings you to this page, where you can download the SDK and the Lightroom Classic SDK Programmers Guide:
The zip file contains:
$ tree -L 2
.
├── API\ Reference
│ ├── index.html
| ...
├── Lua\ Compiler
│ ├── mac
│ └── win
├── Manual
│ └── Lightroom\ Classic\ SDK\ Guide.pdf
├── Readme.txt
└── Sample\ Plugins
├── creatorfilter.lrdevplugin
├── custommetadatasample.lrdevplugin
├── flickr.lrdevplugin
├── ftp_upload.lrdevplugin
├── helloworld.lrdevplugin
├── languagefilter.lrdevplugin
├── metaexportfilter.lrdevplugin
├── mymetadata.lrdevplugin
├── mysample.lrwebengine
└── websample.lrwebengine
Hello World¶
Here is a simple example that creates a custom metadata field:
local LrMetadata = import 'LrMetadata'
LrMetadata:addMetadataField{
id = 'myCustomField',
dataType = 'string',
version = 1,
searchable = true,
browsable = true,
title = 'My Custom Field',
}
In this example, we import the LrMetadata
module and use the addMetadataField
function to add a custom metadata field. The field's id
, dataType
, version
, searchable
, browsable
, and title
parameters are specified in the function call.
Folder Structure¶
Info.lua¶
-- Sam's Lightroom Plug-in
-- @copyright 2022 peddamat
-- @license: TBD
return {
LrSdkVersion = 10.0,
LrToolkitIdentifier = "net.tbd.lightroom.SamsTools",
LrPluginName = "Sam's Tools",
LrPluginInfoUrl = "",
VERSION = {
major = 0,
minor = 1,
revision = 1,
},
LrLibraryMenuItems = {
{
title = "Automatically create collections...",
file = "AutoCollections.lua",
},
{
title = "Export File List for Selected Collection(s)...",
file = "ExportFileListSelected.lua",
},
{
title = "Export Photos from Selected Collection(s)...",
file = "ExportFilesSelected.lua",
},
},
}
SamsTools Plugin¶
As part of a large photo library cleanup project, I ended up writing a few functions which I packaged in the SamsTools plugin:
Export Files Selected¶
This function "Export Photos from Selected Collection(s)..."
-- local common = require "common"
local json = require "json"
local LrApplication = import "LrApplication"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrProgressScope = import "LrProgressScope"
local LrLogger = import 'LrLogger'
local LrFileUtils = import 'LrFileUtils'
local LrPathUtils = import 'LrPathUtils'
-- Create the logger and enable the print function.
local myLogger = LrLogger( 'exportLogger' )
myLogger:enable( "logfile" ) -- Pass either a string or a table of actions.
local function outputToLog( message )
myLogger:trace( message )
end
-- Show save dialog and open file to write.
function get_export_dir()
local directory = import("LrDialogs").runOpenPanel({
title = "Choose the export directory.",
canChooseFiles = false,
canChooseDirectories = true,
canCreateDirectories = true,
allowsMultipleSelection = false,
})
if directory == nil then return nil end -- canceled
-- TODO: Need to make sure directory is writable
-- local fh, err = io.open(filename, "w")
-- if fh == nil then error("Cannot write to the file: " .. err) end
return directory[1]
end
LrFunctionContext.postAsyncTaskWithContext("ExportFilesSelected", function(context)
LrDialogs.attachErrorDialogToFunctionContext(context)
local dst_path = get_export_dir()
if dst_path == nil then return end
outputToLog("Exporting to: " .. dst_path)
local progress = LrProgressScope{title = "Starting photo export..."}
progress:attachToFunctionContext(context)
local results = {}
local catalog = LrApplication.activeCatalog()
-- Get currently selected collections
local sources = catalog:getActiveSources()
-- For each collection...
for _, source in ipairs(sources) do
-- ... print its name
local name = source:getName()
table.insert(results, "Collection: " .. name)
-- ... get its photos
local photos = source:getPhotos()
-- ... batch get the path for all of the photos
progress:setCaption("Retrieving raw metadata for " .. #photos .. "photos in " .. name)
local metadata = catalog:batchGetRawMetadata(photos, {"path"})
if progress:isCanceled() then return end
for i, val in ipairs(photos) do
-- Get file path to photo
local src_path = metadata[val].path
-- Create destination path
local src_name = LrPathUtils.leafName(src_path)
local dst_path = LrPathUtils.child(dst_path, src_name)
progress:setCaption("Exporting: " .. dst_path)
progress:setPortionComplete(i, #photos)
LrFileUtils.copy(src_path, dst_path)
table.insert(results, dst_path)
if progress:isCanceled() then return end
end
end
end)
Export File List Selected¶
This function "Export File List for Selected Collection(s)..."
-- local common = require "common"
local json = require "json"
local LrApplication = import "LrApplication"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrProgressScope = import "LrProgressScope"
local LrLogger = import 'LrLogger'
-- Create the logger and enable the print function.
local myLogger = LrLogger( 'exportLogger' )
myLogger:enable( "logfile" ) -- Pass either a string or a table of actions.
local function outputToLog( message )
myLogger:trace( message )
end
-- Show save dialog and open file to write.
function open_file()
local filename = import("LrDialogs").runSavePanel({
title = "Choose the output filename.",
requiredFileType = "txt",
})
if filename == nil then return nil end -- canceled
local fh, err = io.open(filename, "w")
if fh == nil then error("Cannot write to the file: " .. err) end
return fh
end
LrFunctionContext.postAsyncTaskWithContext("ExportFileListSelected", function(context)
LrDialogs.attachErrorDialogToFunctionContext(context)
local fh = open_file()
if fh == nil then return end
context:addCleanupHandler(function() fh:close() end)
local progress = LrProgressScope{title = "Exporting File Lists..."}
progress:attachToFunctionContext(context)
local results = {}
local catalog = LrApplication.activeCatalog()
-- Get currently selected collections
local sources = catalog:getActiveSources()
-- For each collection...
for _, source in ipairs(sources) do
-- ... print its name
local name = source:getName()
table.insert(results, "Collection: " .. name)
-- ... get its photos
local photos = source:getPhotos()
-- ... batch get the path for all of the photos
progress:setCaption("Retrieving raw metadata for " .. #photos .. "photos in " .. name)
local metadata = catalog:batchGetRawMetadata(photos, {"path"})
if progress:isCanceled() then return end
-- ... print its path
for i, val in ipairs(photos) do
progress:setPortionComplete(i, #photos)
table.insert(results, metadata[val].path )
if progress:isCanceled() then return end
end
end
progress:setCaption("Saving file list.")
for _, line in ipairs(results) do
fh:write(line .. "\n" )
if progress:isCanceled() then return end
end
end)