Skip to content

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":

Screenshot 2023-03-30 at 2.28.38 PM.png

Clicking "View downloads" brings you to this page, where you can download the SDK and the Lightroom Classic SDK Programmers Guide:

Screenshot 2023-03-30 at 2.29.52 PM.png

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

$ cd helloworld.lrdevplugin
$ tree
.
├── Info.lua
└── AddMetadataField.lua

Info.lua

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)


Last update: 2023-03-30