Adobe Photoshop Lightroom SDK 2.2 Guide
Adobe Photoshop Lightroom SDK 2.2 Guide
Adobe Photoshop Lightroom SDK 2.2 Guide
0
PROGRAMMERS GUIDE
Copyright © 2008 Adobe Systems Incorporated. All rights reserved.
Adobe Photoshop Lightroom SDK 2.0 Programmers Guide: December 2008 update for 2.2.
If this guide is distributed with software that includes an end user agreement, this guide, as well as the software
described in it, is furnished under license and may be used or copied only in accordance with the terms of such license.
Except as permitted by any such license, no part of this guide may be reproduced, stored in a retrieval system, or
transmitted, in any form or by any means, electronic, mechanical, recording, or otherwise, without the prior written
permission of Adobe Systems Incorporated. Please note that the content in this guide is protected under copyright law
even if it is not distributed with software that includes an end user license agreement.
The content of this guide is furnished for informational use only, is subject to change without notice, and should not be
construed as a commitment by Adobe Systems Incorporated. Adobe Systems Incorporated assumes no responsibility or
liability for any errors or inaccuracies that may appear in the informational content contained in this guide.
Please remember that existing artwork or images that you may want to include in your project may be protected under
copyright law. The unauthorized incorporation of such material into your new work could be a violation of the rights of
the copyright owner. Please be sure to obtain any permission required from the copyright owner.
Any references to company names in sample templates are for demonstration purposes only and are not intended to
refer to any actual organization.
Adobe, the Adobe logo, Photoshop, Lightroom, and Flash are either registered trademarks or trademarks of Adobe
Systems Incorporated in the United States and/or other countries.
Microsoft and Windows are either registered trademarks or trademarks of Microsoft Corporation in the United States
and/or other countries. Apple, Mac, Mac OS, and Macintosh are trademarks of Apple Computer, Incorporated, registered
in the United States and other countries. Sun and Java are trademarks or registered trademarks of Sun Microsystems,
Incorporated in the United States and other countries. UNIX is a registered trademark of The Open Group in the US and
other countries.
Adobe Systems Incorporated, 345 Park Avenue, San Jose, California 95110, USA. Notice to U.S. Government End Users.
The Software and Documentation are “Commercial Items,” as that term is defined at 48 C.F.R. §2.101, consisting of
“Commercial Computer Software” and “Commercial Computer Software Documentation,” as such terms are used in 48
C.F.R. §12.212 or 48 C.F.R. §227.7202, as applicable. Consistent with 48 C.F.R. §12.212 or 48 C.F.R. §§227.7202-1 through
227.7202-4, as applicable, the Commercial Computer Software and Commercial Computer Software Documentation are
being licensed to U.S. Government end users (a) only as Commercial Items and (b) with only those rights as are granted
to all other end users pursuant to the terms and conditions herein. Unpublished-rights reserved under the copyright
laws of the United States. Adobe Systems Incorporated, 345 Park Avenue, San Jose, CA 95110-2704, USA. For U.S.
Government End Users, Adobe agrees to comply with all applicable equal opportunity laws including, if appropriate, the
provisions of Executive Order 11246, as amended, Section 402 of the Vietnam Era Veterans Readjustment Assistance Act
of 1974 (38 USC 4212), and Section 503 of the Rehabilitation Act of 1973, as amended, and the regulations at 41 CFR
Parts 60-1 through 60-60, 60-250, and 60-741. The affirmative action clause and regulations contained in the preceding
sentence shall be incorporated by reference.
Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
The Lightroom SDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
The Lua language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
About this document . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Conventions used in this document . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3
Contents 4
The Adobe® Photoshop® Lightroom® Software Development Kit (SDK) is a scripting interface to Lightroom
that allows you to extend and customize Lightroom functionality. Use the SDK API to write plug-ins for
Lightroom. This release allows you to customize the behavior of Lightroom’s export operations, define
Lightroom-specific metadata for photos, and create customized HTML web galleries.
http://www.adobe.com/devnet/photoshoplightroom/
The SDK contains these elements (paths are relative to the location that you choose during installation):
7
Preface About this document 8
The Lightroom SDK provides a Lua scripting environment, which extends the Lua languages with an
object-oriented infrastructure; see “The Lightroom SDK scripting environment” on page 10.
➤ Chapter 1, “Using the Lightroom SDK," provides an introduction to the Lightroom SDK, with the basics
of how Lua plug-ins work, and the concepts and terminology of the Lightroom SDK scripting
environment.
➤ Chapter 2, “Writing a Lightroom Plug-in," explains how to use the SDK to create a standard Lightroom
plug-in that extends Lightroom’s export functionality or defines Lightroom-specific metadata.
➤ Chapter 3, “Creating a User Interface for Your Plug-in," explains how to create and populate a dialog
box or a custom section in the Plug-in Manager dialog or Export dialog with user-interface elements,
using the LrView and LrDialogs namespaces.
➤ Chapter 4, “Writing a Web-engine Plug-in," explains how to create a Lightroom plug-in that defines a
new type of web engine. This type of plug-in uses a slightly different architecture.
➤ Chapter 5, “Using ZStrings for Localization," explains how to localize your plug-in’s user interface for
different languages.
Preface About this document 9
➤ Chapter 6, “SDK Sample Plug-ins," walks through the installation and usage of the sample plug-ins
provided with the SDK.
➤ Chapter 7, “Getting Started: A Tutorial Example," walks through a “Hello World” tutorial to help you
create your first plug-in.
➤ Chapter 9, “Web Gallery Plug-ins: A Tutorial Example,” shows how to define your own HTML
web-engine plug-in.
➤ “Writing plug-ins for Lightroom” on page 10 describes the basics of how Lua plug-ins work, including
details of the information file and file-system locations.
➤ “The Lightroom SDK scripting environment” on page 10 explains the concepts and terminology of the
Lightroom SDK scripting environment, and provides details of what tools are available to you within
the SDK scripting environment.
➤ Export functionality: You can create an export plug-in, which customizes the behavior of Lightroom's
Export dialog and export processing. You can add or remove items from the Export dialog, alter or
augment the rendering process, and send images to locations other than files on the local computer.
See “Defining an export service” on page 40 and “Adding an export post-process action” on page 29.
➤ Metadata: You can define customized public or private metadata fields for Lightroom. Public or
private metadata can be associated with individual photos. See “Adding custom metadata” on
page 55.
➤ Web engine functionality: You can create an HTML web-engine plug-in, which defines a new type of
HTML photo gallery. The engines you define appear in the Gallery panel at the upper right of the Web
module. See Chapter 4, “Writing a Web-engine Plug-in.”
A plug-in consists of Lua-language files (scripts) that define the plug-in functionality, and an information
or manifest file that describes the contents of the plug-in. The information file must have a specific name,
and be placed in a folder with the Lua source files and resource files; the folders may need to be in specific
locations. The type of plug-in is determined by the folder and file placement, and by file naming
conventions.
The Lightroom scripting environment provides a programming structure that includes some
enhancements to the basic Lua-language constructs. This section describes the API usage and
terminology.
➤ The API defined for the Lightroom SDK is fully documented in the Lightroom SDK API Reference, which
is part of the SDK. When you have installed the SDK, the home page is at:
LR_SDK_install_location/API Reference/index.html
10
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 11
Lua does not have an object-oriented programming model, but it does allow Lua tables to be used in an
object-like fashion, which the Lightroom SDK does. Lightroom’s object and class model is derived from the
one described in Chapter 16 of "Programming in Lua," available online at http://www.lua.org/pil/16.html.
In Lightroom terminology, object and class are used in the typical object-oriented fashion: that is, a class is
a description of a set of behaviors that are associated with a particular data structure, and an object is a
single instance of that class.
➤ The Lightroom SDK defines a set of namespaces and a set of classes; see “Accessing namespace
functions directly” on page 11 and “Creating objects” on page 12. Plug-ins cannot define either
namespaces or classes.
➤ The Lua language defines built-in namespaces and global functions, of which a subset are accessible
in the Lightroom SDK Lua environment. See “Using built-in Lua features” on page 16.
You can access a namespace by using the built-in function import(); it takes a single parameter, the name
of the namespace to be loaded, and returns the table of functions, which you can then access using dot
notation.
For example:
local LrMD5 = import 'LrMD5' -- assign namespace to local variable
local digest = LrMD5.digest( 'some string' ) -- call "digest()" function in namespace
This example shows the convention of assigning the namespace to a variable of the same name. This
practice is not enforced in any way, but helps avoid confusion.
The Lightroom SDK defines these namespaces; for complete details, see the Lightroom SDK API Reference.
Namespace Description
LrApplication Application-wide information; provides access to the active catalog.
LrBinding Allows you to define data relationships between UI elements and other
objects.
LrDate Allows you to create and manipulate date-time values.
LrDialogs Allows you to show messages in predefined modal dialogs.
LrErrors Allows you to format Lua error strings to be used in error dialogs.
LrExportSettings Allows you to check or set an image file format for an export operation.
LrFileUtils Allows you to manipulate files and folders in the file system in a
platform-independent manner.
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 12
Namespace Description
LrFtp Both a namespace and a class. The namespace functions allow you to work
with the paths and settings for FTP connections created with the LrFtp class.
LrFunctionContext Both a namespace and a class. The namespace functions allows you to make
functions calls with defined methods for cleaning up resources allocated
during the execution of a function or task.
LrHttp Allows you to send and receive data using HTTP. Must be used within a task.
LrLocalization Allows you to localize your plug-in for use in multiple languages.
LrMD5 Provides MD5 digest services.
LrPathUtils Allows you to manipulate file-system path strings in a platform-appropriate
way. (All paths are specified in platform-specific syntax.)
LrPhotoInfo Allows you to get information about individual photo files, such as their
dimensions.
LrPrefs Provides access to application preferences.
LrShell Provides access to shell functions of the platform file browser (Windows
Explorer in Windows or Finder in Mac OS).
LrStringUtils Provides string manipulation utilities.
LrTasks Allows you to start and manage tasks that run cooperatively on Lightroom's
main UI thread.
LrView Both a namespace and a class. The namespace functions allow you to obtain
the factory object, create bindings between UI elements and data tables, and
share placement between UI elements.
LrXml Both a namespace and a class. The namespace functions allows you to create
an XML builder object, and to parse existing XML documents into read-only
XML DOM objects.
Creating objects
When you use the import() function with a class, it returns a constructor function, rather than a table. Use
the constructor to create objects, which you can initialize with specific property values. You can then
access the functions and properties through the object using colon notation.
This example shows the standard way to create and use an object:
local LrLogger = import 'LrLogger'
-- LrLogger is a constructor function, not a table with more functions
local logger = LrLogger( 'myPlugin' )
-- Calling this function returns an instance of LrLogger, which is assigned to
-- local variable logger. Notice the lowercase naming convention for objects.
logger:enable( 'print' )
logger:warn( 'something bad happened' )
-- Method calls on the object that was just created.
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 13
There are some exceptions to this technique. You can create some objects using functions in other objects
or namespaces, such as LrApplication.activeCatalog(). Others are created and passed to you by
Lightroom.
The Lightroom SDK defines these classes; for complete details, see the Lightroom SDK API Reference.
A few classes (LrFtp, LrView, LrXml, and LrFunctionContext) act as both classes and namespaces, and
allow you to call some functions directly in the imported namespace, using dot notation. By convention,
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 15
the documentation uses lowercase names, as well as colon notation, to indicate that a function is called on
an instance. For example:
LrFtp.appendFtpPaths() -- A namespace function
ftpConnection:path() -- An object function
Classes define both functions and properties. To access properties in objects, use the dot notation. Again,
the documentation uses the lowercase naming convention to indicate an instance of a class:
exportRendition.photo -- An object property
A property can have no value; a nil property value is not the equivalent of false, zero, or the empty string.
Setting a nil value for a property that has a default value causes the property to revert to the default.
The LrFunctionContext namespace and class is a programming utility for error handling.
Use LrFunctionContext.callWithContext() to wrap a function call. This allows you to register an error
handler for the call, trapping any errors that occur during the execution of the wrapped function.
Lightroom provides predefined error dialogs that you can customize with explanatory messages, as shown
in the following example.
This shows the predefined error dialog with customized text, according to how the error was thrown:
The Lua language defines built-in namespaces and global functions, of which only a subset are supported
in the Lightroom SDK Lua environment, as follows:
➤ Available in Lightroom:
assert(), dofile(), error(), getmetatable(), ipairs(), load(), loadfile(),
loadstring(), next(), pairs(), pcall(), rawequal(), rawget(), rawset(), select(),
setmetatable(), tonumber(), tostring(), type(), unpack()
➤ Partially available:
➣ os: Contains only the functions clock(), date(), time(), and tmpname(). All other functions
removed. Use LrFileUtils, LrDate, and LrTasks instead.
➣ table: Contains all functions except getn(), setn(), and maxn(), which are deprecated as of Lua
5.1.
Defining tasks
Your plug-in can use the LrTasks functions to create and manage a task, which is a kind of lightweight
process that runs cooperatively on Lightroom's main (user interface) thread. If your service defines a
lengthy export operation that would block the main Lightroom process, you should run it as a background
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 17
task, using the function LrTasks.startAsyncTask(). Some API functions, such as those in the LrHttp
namespace, are only available when called from within a background task.
In general, you are responsible for creating tasks when needed. There is one exception; the
processRenderedPhotos function that you define for an export service or export filter provider is called
from within a task that Lightroom starts. See Chapter 2, “Writing a Lightroom Plug-in."
For details of the LrTasks functions, see the Lightroom SDK API Reference.
A script to be executed this way typically has the effect of defining a table containing a suite of functions.
For example:
SomeFile.lua
SomeFile = {}
-- Typically a file that is required will define a global table whose name
-- matches the file name.
-- Note that this global is defined in a special function environment for your
-- plug-in and does not affect Lightroom as a whole.
-- You can give this table any name that does not conflict with built-in names
-- and keywords from Lua and Lightroom. In general, avoid names that start with
-- Lr to avoid conflicts with future versions of Lightroom.
function SomeFile.doSomething( arg )
return tostring( arg )
end
Usage of require()
require 'SomeFile.lua'
-- Causes SomeFile.lua to be executed and the value of SomeFile defined above
-- becomes available in the scope of this file.
SomeFile.doSomething( 42 )
➤ Literal strings can be surrounded by either single or double quotes. These two statements are
equivalent:
local a = 'my string'
local a = "my string"
➤ If you call a function with a single parameter that is a string literal or a table, you can omit the
parentheses around the argument list. This is frequently done when calling the built-in functions
import() and require().
CHAPTER 1: Using the Lightroom SDK The Lightroom SDK scripting environment 18
These three statements are equivalent (where func is a variable containing a valid function):
func( "foo" )
func "foo"
func 'foo'
These two statements are also equivalent; the simpler syntax is commonly used when building view
descriptions:
func( { a = 1, b = 2 } )
func{ a = 1, b = 2 }
➤ Lua defines an array as a table with numbered keys. Arrays in Lua are 1-based; that is, the first item in
the array is at index 1, not index 0.
➤ The value nil evaluates to a Boolean value of false, but numbers (including 0) evaluate to true. Thus,
in a conditional, only nil and false are considered false. If you use 0 as the condition of an if or
while statement for example, the statement is executed, because the number 0 is a true value.
➤ Lightroom defines Boolean globals, WIN_ENV and MAC_ENV, which you can use to determine which
platform your script is running on.
2 Writing a Lightroom Plug-in
The Lightroom SDK allows you to customize the behavior of Lightroom in specific ways. Most types of
plug-in share a common architecture, which is discussed in this chapter. Web Gallery plug-ins use a
different architecture in this release; see Chapter 4, “Writing a Web-engine Plug-in.”
➤ The Plug-in Manager dialog allows a user to load plug-ins from any location, enable and disable
loaded plug-ins, and remove unused plug-ins. Your plug-in can customize the dialog by adding
sections.
➤ Export plug-ins allow you to customize Lightroom's export behavior. You can:
➣ Alter the rendering process, or define post-processing actions for rendered photos.
➣ Send rendered images to locations other than files on the local computer.
➤ You can also use a plug-in to define customized metadata fields for photos.
➤ Your plug-in can intercept the export process with an Export Filter Provider, which can apply further
processing to photos that the user has chosen to export. The post-process action that a filter defines is
applied after Lightroom’s initial rendering, and before the photos are sent to the final destination. Each
filter can add a section to the Export dialog, in which the user can select options and set parameters.
See “Adding an export post-process action” on page 29.
➤ Your plug-in can add a new export destination with an Export Service Provider; which can also
customize the Export dialog by adding and removing sections as appropriate for the destination when
the user selects it. See “Customizing the export destination” on page 40.
➤ You can add items to the File, Library, or Help menus to start a task (process) that performs an export
operation.
➤ In addition to or instead of defining export customizations, your plug-in can define custom metadata
fields for Lightroom. See “Adding custom metadata” on page 55.
19
CHAPTER 2: Writing a Lightroom Plug-in Writing standard plug-ins for Lightroom 20
LrSdkVersion Required The preferred version of the Lightroom SDK for this plug-in.
Should be set to 2.0; older plug-ins may have a value of 1.3
number or 1.4.
LrSdkMinimumVersion Optional The minimum version of the SDK that this plug-in can use. If
your plug-in works with Lightroom 1.3 but it also provides
number new features specific to Lightroom 2.0, set this value to 1.3
and LrSdkVersion to 2.0. Default is the value of
LrSdkVersion.
LrToolkitIdentifier Required A string that uniquely identifies your plug-in. Use Java-style
package names (your domain name in reversed sequence).
string
You can use the Plug-in Manager to add multiple plug-ins
with the same identifier, but only one of them can be
enabled. If you enable one, any other plug-in that shares the
same plug-in ID is automatically disabled.
LrPluginInfoUrl Optional The URL of your web site, or a page that provides
information about your plug-in. See “Customizing plug-in
string load behavior” on page 26
major (number)
minor (number )
revision (number)
build (number)
display (string)
LrExportFilterProvider Optional Adds one or more new export filters, which can process
photos before they are rendered for the export destination.
table of
tables Each item is a table with these entries:
NOTE: The Info.lua file is a special Lua environment that is much more restrictive than the general SDK
Lua environment in which other scripts run. The standard Lua namespace string is available, and you can
use the LOC function for localization of display strings in this file (see Chapter 5, “Using ZStrings for
Localization"). You can also use WIN_ENV and MAC_ENV environment variables.
However, you cannot use any of the other Lua or Lightroom globals defined in the SDK scripting
environment, (see “The Lightroom SDK scripting environment” on page 10). For example, you cannot use
import or require in this context.
Here is an example of an Info.lua file for a plug-in that adds items to the Lightroom menus:
return {
LrSdkVersion = 2.0,
LrToolkitIdentifier = 'com.adobe.lightroom.export.flickr',
-- your plug-ins will use your own domain name, not com.adobe.*
LrLibraryMenuItems = {
title = LOC "$$$/Flickr/ExportUsingDefaults=Export to Flickr Using Defaults",
file = 'ExportToFlickr.lua',
enabledWhen = 'photosAvailable',
},
LrExportMenuItems = {
{
title = LOC "$$$/Flickr/EnterAPIKey=Enter Flickr API Key...",
file = 'EnterApiKey.lua',
},
{
title = LOC "$$$/Flickr/ExportUsingDefaults=Export to Flickr Using Defaults",
file = 'ExportToFlickr.lua',
enabledWhen = 'photosAvailable',
},
},
LrExportServiceProvider = {
title = LOC "$$$/Flickr/Flickr=Flickr",
file = 'FlickrExportServiceProvider.lua',
builtInPresetsDir = "presets",
},
}
CHAPTER 2: Writing a Lightroom Plug-in Writing standard plug-ins for Lightroom 24
You can find more examples in the sample plug-ins provided with the SDK.
NOTE: In Mac OS, the suffix .lrplugin creates a package, which looks like a single file. For convenience,
you can use the suffix .lrdevplugin during development, and change the extension to .lrplugin for
delivery. The .lrdevplugin suffix is recognized by Lightroom but does not trigger the package behavior
in the Mac OS Finder.
A plug-in folder can reside in any location on the hard drive. Users can load the plug-in using the Add
button in the Plug-in Manager. Once it is added, users can enable or disable it through the dialog, reload it
using the Plug-in Author Tools in the dialog, or unload it using the Remove button.
Your plug-in can customize the Plug-in Manager, adding sections that appear when a user selects your
plug-in in the dialog. For example, in the following figure, the plug-in called "Plug-in Info Sample" defines
two custom sections, one above and one below the Lightroom-defined sections. They appear only when
the plug-in is selected in the list. The sections are collapsible, and you can define a descriptive string (a
synopsis) to appear on the right side when the section is closed.
CHAPTER 2: Writing a Lightroom Plug-in Writing standard plug-ins for Lightroom 25
custom section
For details of how to define these sections, see See “Adding custom sections to the Plug-in Manager” on
page 28.
Lightroom automatically checks for plug-ins in the standard Modules folder where other Lightroom
settings are stored:
You may want to use this location if, for example, you are writing an installer that installs a Lightroom
plug-in and also installs a helper application.
Plug-ins that are installed in this location are automatically listed in the Plug-in Manager dialog. You can
use the dialog to enable or disable such a plug-in, but not to remove it. The Remove button is dimmed
when such a plug-in is selected.
CHAPTER 2: Writing a Lightroom Plug-in Customizing plug-in load behavior 26
This section is generally not needed by end users, and is closed by default. If you open the "Plug-in Author
Tools" section, you can:
➤ Choose to have Lightroom reload the plug-in automatically on each export operation.
NOTE: Reloading a plug-in interactively or automatically after export does not reload any localization
dictionaries supplied with that plug-in. The translation dictionaries are read only when the plug-in is
first loaded or Lightroom is restarted. See Chapter 5, “Using ZStrings for Localization.”
➤ Choose a file to which to save diagnostic messages if a plug-in fails to load, or encounters an error at
any stage of its operation.
The Lightroom SDK does not supply a development environment in which to debug your plug-ins, but it
does supply the LrLogger namespace, which allows you to configure a log file and viewer for trace
information of various kinds, and add tracing statements to your scripts. For an example, see “Debugging
your plug-in” on page 167.
➤ LrPluginInfoProvider points to a script that can return any or all of the following function
definitions which customize appearance or behavior of the Plug-in Manager dialog when the plug-in
is selected:
Item Description
startDialog Initialization and termination functions that run when your plug-in
endDialog is selected or deselected in the Plug-in Manager dialog.
Item Description
sectionsForTopOfDialog Definitions for one or more new sections to display in the Plug-in
sectionsForBottomOfDialog Manager dialog when your plug-in is selected in the dialog.
➤ LrInitPlugin points to a script that runs when the plug-in is loaded or reloaded. You can use this to
initialize values in your plug-in’s global function environment, which are protected from garbage
collection. When the plug-in is reloaded, a new environment is created. All previous values are
discarded and this function is executed again.
➤ LrPluginInfoUrl gives the URL of your web site, or a page that provides information about your
plug-in. This URL is displayed in the Status section of the Plug-in Manager dialog when your plug-in is
loaded and selected. The URL is also displayed as part of the error message if your plug-in fails to load
properly or cannot be found.
For example:
return {
LrSdkVersion = 2.0,
LrSdkMinimumVersion = 2.0, -- minimum SDK version required by this plug-in
LrToolkitIdentifier = 'com.adobe.lightroom.sample.plug-in-info',
LrPluginName = LOC "$$$/PluginInfo/Name=Plug-in Info Sample",
LrPluginInfoProvider = 'PluginInfoProvider.lua',
LrInitPlugin = 'PluginInit.lua',
LrPluginInfoUrl = 'http://www.mycompany.com/lrplugin_info.html',
},
}
This example states explicitly that the minimum SDK version is 2.0. This is the default case, so the entry is
not really necessary; it is included only to document the fact that the plug-in is not meant for earlier
versions.
➤ The startDialog function is called whenever your plug-in is selected in the Plug-in Manager.
➤ The endDialog function is called when the user deselects the plug-in in the Plug-in Manager.
NOTE: These same entries can be supplied by an Export Service Provider, although the definitions are
slightly different. Functions defined in an Export Service Provider are executed only when the plug-in is
selected in the Export dialog, never from the Plug-in Manager dialog. See “Initialization and termination
functions for the Export dialog” on page 43.
CHAPTER 2: Writing a Lightroom Plug-in Customizing plug-in load behavior 28
The propertyTable parameter for both functions is an empty, observable table which you can use to keep
private data for your plug-in. (See “Binding UI values to data values” on page 79.) This table is discarded
when your plug-in is deselected in the Plug-in Manager or when the Plug-in Manager dialog is closed. It is
not preserved across sessions. You can use LrPreferences if you want to save information across
invocations.
These are blocking calls. If you need to start a long-running task (such as network access), create a task
using the LrTasks namespace. See “Defining tasks” on page 16.
To customize the dialog, define a function that returns a table of sections, defined using LrView objects.
The function is the value of one of these service entries:
sectionsForTopOfDialog = function( viewFactory, propertyTable ) ... end,
sectionsForBottomOfDialog = function( viewFactory, propertyTable ) ... end,
NOTE: Similar functions can be defined in an Export Service Provider, to customize the Export dialog
when the export destination is selected, and in an Export Filter Provider, to add a section to the Export
dialog when a post-process action is selected. See “Adding custom dialog sections to the Export
dialog” on page 43 and “Adding an export post-process action” on page 29.
Lightroom passes your function a factory object which allows you to create the LrView objects that define
the elements of your sections; that is, UI controls, such as buttons and text, and containers that group the
controls and determine the layout. For additional details of the dialog elements you can create with
LrView, see “Adding custom dialog views” on page 68.”
The function that you define here returns a table of tables, where each table defines one dialog section:
sectionsForTopOfDialog = function( viewFactory, propertyTable )
return {
{ ...section entry ... },
{ ...section entry ... },
...
}
}
end
A section entry table defines the contents of an implicit container, which Lightroom creates to hold your
view hierarchy.
➤ Each section entry sets a title and synopsis for the section; the section is identified by the title
text on the left, and is collapsible. When in the collapsed state, the synopsis text is shown on the
right.
➤ The rest of the table entry creates the UI elements that are shown when the section is expanded. To
create the UI elements, use the LrView factory passed to your top-level sectionsFor... function.
This process is explained in more detail in “Adding custom dialog views” on page 68.
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 29
custom section
When adding sections to the Plug-in Manager, the propertyTable parameter for both functions is an empty,
observable table which you can use to keep private data for your plug-in for a dynamic user interface. See
“Binding UI values to data values” on page 79.
A single SDK export plug-in can define one or more Export Filter Providers, one or more Export Service
Providers, or both. In any given export session, there must be exactly one Export Service Provider, but
there can be any number of post-process actions (or none). A single Export Filter Provider can define
multiple actions. Post-process actions are executed in a specific sequence, partly determined by user
choices. If you set up a dependency using the requiresFilter option, the sequence of execution honors
that dependency.
While Export Service Providers can add multiple sections at either the top or the bottom of the Export
dialog, each post-process action can provide only one section for the dialog, which is always inserted after
Lightroom's built-in sections, and before any "bottom" sections defined by the Export Service Provider.
A post-process action is inserted between Lightroom's initial rendering of photos and the writing of the
rendered image files to their destination (either the default destination, or one provided by a plug-in’s
export service). A post-process action (or set of actions) can be applied to photos that are being exported
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 30
to any destination; that is, an Export Filter Provider does not need to be part of the same plug-in that
provides the export service.
An action that has been inserted is flagged with a check mark; when it is selected, the Remove button is
enabled, allowing you to remove it from the processing queue.
When an action is inserted, the related section is shown in the Export dialog. You can also remove the
action from the queue by clicking the X icon in the related section.
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 31
Action dependencies
You can set up a dependency among a set of actions, such that one action actually performs the photo
processing, and other actions in the set are used to determine the parameters for that operation. The one
that performs the rendition is typically the only one that defines a filterRenderedPhotos() function.
This main action is required by the others in the set. To declare the dependency, make the ID of the main
action the value of the requiresFilter option for the dependent actions.
Each post-process action can define a single section for the Export dialog. When the user selects an action,
that action’s dialog section is shown, along with that of the required filter, if there is one.
When the user chooses the FTP-upload export destination and clicks Export, the service provider requests
an export rendition for each photo that is active at the time. If the user does not choose any actions, the
request is satisfied directly by Lightroom using LrExportRendition.
If the user inserts Color, the dialog shows both the section for defining a border, and the section for
MyAction, which is required by Color. After making all the necessary choices for the chosen actions, the
user clicks Export. In this case, the request is intercepted and redirected to Color. Color receives a list of
renditions that it is expected to satisfy; the action than makes its own rendition request. This request is
similarly intercepted and sent to MyAction. MyAction performs the actual processing and makes its own
request for renditions. When there are no more actions, the requests are satisfied directly by Lightroom
using LrExportRendition.
Each action runs in its own task in Lightroom (see “Defining tasks” on page 16), which means that the
operations performed by each action can be performed in parallel. An action task first requests its
renditions, then iterates through them making its transformations as appropriate. When the action is done
rendering each photo, it signals the downstream task which can then process the rendered photo. For a
more detailed description of the processing path, see “How post-process actions are executed” on
page 35.
To declare one or more post-process actions, add the following block to your Info.lua file:
LrExportFilterProvider = {
title = "Filter Name", -- this display string appears in the dialog
file = "MyExportFilterProvider.lua", -- the action definition script
id = "myFilter", -- a unique identifier for the action
requiresFilter = "mainFilter" -- optional
},
There can be one or many action definitions. Each definition is a table with up to four items:
➤ title (string): The localizable display name of the action, which appears in the Post-Process Actions
section of the Export dialog.
➤ file (string): The name of the Lua file (action definition script) that provides more information about
the action. The script is executed when the export operation is started; that is, when the user clicks
Export in the Export dialog.
➤ id (string): An identifying string for this action, unique within this plug-in. Required if more than one
action is defined in one plug-in.
➤ requiresFilter (string): Optional, the identifier for the action that performs the processing.
For example, this defines three distinct actions, and the first is the one that actually performs the
processing. It must be present before either of the other two can run; they simply set parameters to be
used by the main action:
LrExportFilterProvider = {
{
title = "MyAction",
file = "myAction.lua",
id = "main",
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 33
},
{
title = "Color",
file = "colorAction.lua",
id = "color",
requiresFilter = "main",
},
{
title = "Lines",
file = "lineAction.lua",
id = "lines",
requiresFilter = "main",
},
},
postProcessRenderedPhotos A function that defines how this action processes the list of rendered
photos that it receives. See “Defining post-processing of rendered
photos” on page 34.
The property table passed to this function is shared among all Export
Filter Providers and Export Service Providers defined by this plug-in.
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 34
The function should return true if the photo should remain in the list and be passed to the next action or
exported, or false if it should be removed from the list.
This example is for a simple filter that removes photos that do not have a minimum star rating:
function RatingExportFilterProvider.shouldRenderPhoto( exportSettings, photo )
local minRating = exportSettings.min_rating or 1
local shouldRender
photo.catalog:withReadAccessDo( function()
local rating = photo:getRawMetadata( 'rating' )
shouldRender = rating and rating >= minRating
end )
return shouldRender
end
This function takes two parameters, a function context and a filter context. It can retrieve each photo from
the filterContext.renditionsToSatisfy property, and process it as desired. The list of renditions
provides access to the export settings with which the photos were originally rendered, and can check or
modify those settings and rerender the photos as needed.
The processing is typically performed by an external application. You can build a command string and
pass it to a platform-specific shell for execution, using LrTasks.execute().
-- Optional: If you want to change the render settings for each photo
-- before Lightroom renders it, write something like the following.
-- If not, omit the renditionOptions definition, and also omit
-- renditionOptions from the call to filterContext:rendition()
local renditionOptions = {
filterSettings = function( renditionToSatisfy, exportSettings )
exportSettings.LR_format = TIFF
return os.tmpname()
-- ... if you wanted Lightroom to generate TIFF files.
-- By doing so, you assume responsibility for creating
-- the file type that was originally requested and placing it
-- in the location that was originally requested in your
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 35
The following Export Dialog shows three Export Filter Providers, whose actions have all been inserted in
the processing queue. The Export Service Provider for FTP Upload (one of the sample plug-ins included in
the SDK) has been selected.
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 36
The post-process actions are always invoked in the order in which they appear in the dialog, but the
export operation traverses the stack several times, either top-to-bottom (blue arrows), or bottom-to-top
(red arrows). In this discussion, the terms upstream and downstream refer to the downward flow; for
example, when photos that have been rendered by the built-in render engine (using information passed
up from the providers) are passed back down to be modified, and finally exported:
➤ An upstream provider means the post-process action immediately above the current one in the dialog,
which provides a rendered photo to the current action for further processing. When there are no more
actions, the final upstream provider is Lightroom’s built-in rendering engine.
➤ A downstream consumer means the post-process action immediately below the current one in the
dialog, which receives a rendered photo from the current one, its upstream provider. When there are
no more actions, the final downstream consumer is the Export Service Provider that sends the
rendered photo to the final destination.
When the user starts the export operation by clicking Export, Lightroom constructs an LrExportSession
object with the settings and photos chosen in the dialog.
In this discussion, an Export Service Provider is called a service, and an Export Filter Provider is called a filter.
If the service has defined an updateExportSettings() function, it is called. This function take one
argument, the export-settings table, and allows the service to force certain render settings to its preferred
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 37
or required settings. For example, to force a specific size for the exported photos, you could use this
definition:
updateExportSettings = function( exportSettings )
exportSettings.LR_size_maxHeight = 400
exportSettings.LR_size_maxWidth = 400
end
➤ If it is defined, the shouldRenderPhotos() function is called for each photo. If it returns false, the
photo is removed from the list of photos to export (and thus does not get passed to the downstream
consumer). If it returns true, the photo remains in the list and is passed to the downstream consumer.
➤ If shouldRenderPhotos() is not defined for the filter, all photos are passed to the downstream
consumer.
1. The export session (LrExportSession) generates rendition request objects (LrExportRendition) for
every photo that was not removed in Stage 2. (The actual rendering of the photos does not start yet.)
During this stage, Lightroom can show a dialog message if a photo already exists at the proposed
destination location. You can control this behavior using the LR_collisionHandling setting in the
export settings table.
2. The service's processRenderedPhotos() function is called. If no such function exists, a default one is
provided that performs the steps described below.
IMPORTANT: Each of the providers (the export service, filters, and Lightroom's built-in rendering engine)
runs in its own task (using LrTasks), so these loops operate in parallel. It is likely that each provider will be
running simultaneously. Several photos can be in process simultaneously by different providers. This
allows the overall export operation to complete much more quickly than it would if every photo had to go
through all of the steps before the processing of the next photo could begin.
4. The service's rendition requests are sent to its upstream provider (that is, the bottom-most filter, or if
there are no filters, Lightroom's built-in render engine).
➣ The filter context object generates a new rendition request (LrExportRendition) for each of the
renditions provided this filter.
CHAPTER 2: Writing a Lightroom Plug-in Adding an export post-process action 38
➣ The renditions() iterator provides two values: sourceRendition (the new rendition to be
satisfied by the upstream provider) and renditionToSatisfy (the corresponding rendition that
this filter is expected to satisfy for its downstream consumer).
ADVANCED: If the filter provider wishes to request a different file format than it is expected to satisfy, it can
do so using the renditionOptions/filterSettings code snippet shown in “Defining post-processing
of rendered photos” on page 34. This might be a good idea as a way to avoid re-encoding (and thus
degrading) JPEG files.
➣ The filter must wait for each rendition to be completed by its upstream provider by calling
sourceRendition:waitForRender(). (We will discuss the completion of this loop in Stage 4.)
If there is no processRenderedPhotos() function defined by this filter, the filter is simply left out of
the loop and all rendition requests instead go to the upstream provider.
IMPORTANT: Each filter must generate a photo conforming exactly to the specifications provided to it. In
particular, it must provide a suitable photo file in the specified format and at the exact path specified by
the downstream consumer. If it cannot do so, it must use renditionToSatisfy:renditionIsDone(
false, message ) to indicate why not. It must never provide a file of a different format than that
requested.
6. After all of the filters have had an opportunity to intercept the rendition requests, the requests are
finally passed to Lightroom's built-in rendering engine.
As Lightroom completes each rendition request, it signals completion by allowing the corresponding
rendition:waitForRender() call to complete.
The rendering loops described in stage 3 then finish in top-to-bottom sequence for each photo. For each
filter that defines the postProcessRenderedPhotos() function:
1. The waitForRender() call completes, meaning that the upstream provider has completed its attempt
to render the photo. If that attempt was successful, a valid photo file conforming to the specifications
requested by this filter is present at the path specified by the sourceRendition; that is, specified by
this filter when it requested the rendition from its upstream provider.
2. The filter can now do whatever processing it needs to do on that file. This typically means invoking a
third-party application using LrTasks.execute().
ADVANCED: If the filter has changed the file format or location on disk using renditionOptions, it must
now perform the appropriate operations on the file to convert it so that it now satisfies the request as
specified in renditionToSatisfy.
3. When the processing operation is finished, the filter must report its status on the rendition by calling
renditionToSatisfy:renditionIsDone( success, message ).
This is done automatically by the filterContext:renditions() iterator if you have not already
done so explicitly. The iterator verifies that a file exists at the expected path and signals success or
failure accordingly. If the file is missing, it uses a generic "an unknown error occurred" message. If you
want to provide a more meaningful message, make an explicit call to renditionIsDone( false,
message ). Typically, you need only call renditionIsDone() on failure.
5. Meanwhile, the task for this filter continues on and waits for the next rendition.
6. Once all of the filters have finished processing a photo, the waitForRender() call in the service’s
processRenderedPhotos loop completes. The service does whatever processing it needs to (in this
example, uploading with FTP), and then waits for the next photo to be available.
7. If the "Add to this Catalog" checkbox was selected, Lightroom adds the new photo to the catalog at
this point.
Once the service’s processRenderedPhotos loop completes, Lightroom takes the following clean-up
actions:
➤ If the photo files were rendered into a temporary folder, Lightroom deletes the folder and its contents.
➣ If any photos failed to export, shows an error dialog that summarizes all of the errors encountered
while exporting.
➣ Creates a temporary "Previous Export" collection with the source photos that were exported.
These behaviors are not available when an export is initiated by calling LrExportSession directly.
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 40
The section at the top right of the Export dialog, labeled "Export xx selected photos to", shows the selected
destination:
Your plug-in can provide an export service to allow the user to choose a different destination, such a
remote site, and define how the image files are sent to that destination; by FTP upload, for example.
A plug-in that includes an Export Service Provider gives the user a further choice of destinations for the
export operation. For example, a plug-in can add a Web service such as Flickr as the destination, so that
the export operation uploads the selected files to Flickr, using the settings that the user selects in the rest
of the dialog. When the plug-in is loaded, the user can select the new destination using the up or down
arrow at the right.
If you provide a new export destination, you typically also need to add settings that are meaningful for
your customized export operation. Your plug-in can define customizations for other parts of the Export
dialog, which are shown when the user selects your Export destination.
➤ If your upload operation requires more complex user choices, you can add new sections to the Export
dialog, with the UI elements that the user will need to make those choices. See “Adding custom dialog
sections to the Export dialog” on page 43.
➤ The User Presets list at the left can include presets that you define and include with your plug-in, as
well as those defined by Lightroom for export operations; see “Remembering user choices” on
page 46.
To declare an Export Service Provider, add the following block to your Info.lua file:
LrExportServiceProvider = {
title = "Service Name", -- this string appears as the Export destination
file = "MyExportServiceProvider.lua", -- the service definition script
builtInPresetsDir = "myPresets", -- an optional subfolder for presets
},
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 41
The title and file entries are required. You can use the built-in function LOC and a ZString if you wish to
localize the service’s title; see details in Chapter 5, “Using ZStrings for Localization.”
➤ Branding options for customizing the look of the destination area at the top of the Export dialog when
your service is selected. (Some of these can appear in the Info.lua file instead; see “Branding your
export service” on page 47).
➤ One or more items that define the desired customizations for the Export dialog. These types of
customizations are defined:
➤ A function that defines the export operation to be performed on rendered photos (required).
These are the specific items that can be in the table returned by the service definition script for an Export
Service Provider:
Item Description
startDialog Initialization and termination functions for your plug-in; see
endDialog “Initialization and termination functions for the Export dialog” on
page 43.
exportPresetFields A set of export preset settings that you define for your plug-in (in
addition to the built-in settings defined by Lightroom). See
“Remembering user choices” on page 46.
processRenderedPhotos A function that manages the rendering and subsequent handling
of exported photos; see “Final processing of rendered photos” on
page 50.
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 42
Item Description
canExportToTemporaryLocation A Boolean value that indicates whether the service provider can
place files in a temporary export destination in the local file
system.
If the user selects this option, the file naming options in the dialog
disappear and the files are written to a hidden temporary folder
on the local hard drive. When the Export Service Provider has
completed its work, this folder and its contents are deleted.
If your plug-in hides the Export Location section of the dialog, you
do not need to use this option. The temporary folder behavior
happens automatically in that case.
➤ The startDialog function is called when the user chooses a post-process action or export destination
provided by this plug-in in the Export dialog, or when the destination is already selected when the
dialog is invoked, remembered from the previous export operation.
➤ The endDialog function is called when the user deselects the action or export destination in the
Export dialog, or dismisses the Export dialog.
NOTE: Similar entries can be supplied by a Plug-in Info Provider, although the definitions are slightly
different. Functions defined in a Plug-in Info Provider are executed only when the plug-in is selected in the
Plug-in Manager dialog, never from the Export dialog. See “Initialization and termination functions for the
Plug-in Manager” on page 27.
The propertyTable parameter for both functions is a table which contains the most recent settings for your
export plug-in, including both settings that you have defined and Lightroom-defined export settings (see
“Remembering user choices” on page 46).
When your plug-in is deactivated through the Export dialog, Lightroom calls your endDialog function
with why set to one of the following string values:
changedServiceProvider A different Export Service Provider was chosen as the export destination.
Your plug-in is no longer active.
ok The user clicked the "Export" button. The Export dialog has closed, and
Lightroom will begin exporting images through your plug-in.
These are blocking calls. If you need to start a long-running task (such as network access), create a task
using the LrTasks namespace. See “Defining tasks” on page 16.
To customize the dialog, define a function that returns a table of sections, defined using LrView objects.
The function is the value of one of these service entries:
sectionsForTopOfDialog = function( viewFactory, propertyTable ) ... end,
sectionsForBottomOfDialog = function( viewFactory, propertyTable ) ... end,
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 44
NOTE: Similar functions can be defined in a Plug-in Info Provider, to customize the Plug-in Manager
dialog, and in an Export Filter Provider, to add a section to the Export dialog when a post-process
action is selected. See “Adding custom sections to the Plug-in Manager” on page 28 and “Adding an
export post-process action” on page 29.
Lightroom passes your function a factory object which allows you to create the LrView objects that define
the elements of your sections; that is, UI controls, such as buttons and text, and containers that group the
controls and determine the layout. For additional details of the dialog elements you can create with
LrView, see Chapter 3, “Creating a User Interface for Your Plug-in.”
The function that you define here returns a table of tables, where each table defines one dialog section:
sectionsForTopOfDialog = function( viewFactory, propertyTable )
return {
{ ...section entry ... },
{ ...section entry ... },
...
}
}
end
A section entry table defines the contents of an implicit container, which Lightroom creates to hold your
view hierarchy.
➤ Each section entry sets a title and synopsis for the section; the section is identified by the title
text on the left, and is collapsible. When in the collapsed state, the synopsis text is shown on the
right:
➤ The rest of the table entry creates the UI elements that are shown when the section is expanded. To
create the UI elements, use the LrView factory passed to your top-level sectionsFor... function.
This process is explained in more detail in Chapter 3, “Creating a User Interface for Your Plug-in.”
When adding sections to the Export dialog, the propertyTable parameter for both functions is the property
table containing the plug-in and Lightroom-defined export settings. You can add your own program data
values to this table, and create bindings between the UI elements and the data values, so that the UI text is
dynamically tied to your plug-in data. This is shown in the example below, and explained more fully in
“Binding UI values to data values” on page 79.
This sample code creates a section in the destination dialog with two UI controls, an editable text field and
a button. These are in a container, a row element which controls the placement of its child nodes, but is
not drawn on the screen.
The value of the edit field is tied to a data key in the property table. The enabled state of the button is also
tied to a data key, so that the button is only enabled when the user types into the edit field, thus setting
the data value.
In this example, the synopsis text is also dynamic, bound to the same data key as the edit field value.
(Currently, you cannot bind the title value.) Notice that for synopsis, you must specify the bound table
explicitly. This is because it is not part of the view hierarchy, where the propertyTable is automatically
assigned as the default bound table.
All of these concepts and techniques are explained more fully and described in more detail in Chapter 3,
“Creating a User Interface for Your Plug-in."
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 45
This code creates this custom section at the top of the Export dialog (when defined by an
LrExportServiceProvider entry):
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 46
Export settings (both plug-in-defined and Lightroom-defined) are saved from one invocation of the Export
dialog to the next, and across Lightroom sessions. The most recent settings values are passed to your
initialization function when the plug-in is invoked, to various service-script functions (such as
startDialog, sectionsForTopOfDialog, and so on), and to your plug-in’s termination function.
To declare properties as plug-in-specific export settings, the service definition script should return this
item:
exportPresetFields A table of keys and associated default values. These are automatically
stored in both Lightroom preferences and Export presets.
The default values are used only on the first invocation of your plug-in;
after that, the previously set values are restored.
For example:
exportPresetFields = {
{ key = 'privacy', default = 'public' },
{ key = 'privacy_family', default = false },
{ key = 'privacy_friends', default = false },
{ key = 'safety', default = 'safe' },
{ key = 'hideFromPublic', default = false },
{ key = 'type', default = 'photo' },
{ key = 'addTags', default = '' },
}
The settings you declare here are automatically saved along with the export settings already defined by
Lightroom (see “Lightroom built-in property keys” on page 51). The first time your plug-in is activated, the
default value is used to initialize your settings. On the second and subsequent activations, the settings
chosen by the user in previous sessions are restored.
The user can choose to save a particular configuration of settings values as a preset. A preset contains all of
the current settings values, including both Lightroom-defined and plug-in-defined fields.
If you wish to create a predefined preset for you plug-in, to be loaded along with your plug-in and appear
in the Lightroom Presets list, you must:
3. Right-click (control-click in Mac OS) on the newly-created Preset to find the preset file. Move the
preset file from that folder to the subfolder that you specified using the builtInPresetsDir entry for
the LrExportServiceProvider entry in the Info.lua file for your plug-in.
All of the branding entries (except title) are optional, and can be defined by entries in the table returned
by your service script. Those that do not specify colors can be specified directly in the
LrExportServiceProvider entry in the Info.lua file. (The LrColor object is not available in the context
of the plug-in information file.)
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 48
title_color LrColor The color for the title text, if shown in the dialog. An LrColor object.
When supplied, this icon is displayed to the left of the title text. It
should be no higher than 44 pixels and no wider than 576 pixels; it is
centered vertically.
image string or The name of a PNG image file in this plug-in’s folder (or a function
function that loads and returns a PNG image file).
When supplied, this image is used instead of the title text in the
Export destination section when your service is selected.
title_x_origin number The exact location, relative to the left of the pop-up control, to
which the title text is aligned. Best used with a medium icon whose
alignment is flush_left.
A single toggle entry controls whether users can select measurement units:
hidePrintResolution = Boolean When true, the options for sizing in the Image Sizing section
are shown only in pixel units; all mention of print units such as
inches, centimeters, and pixels-per-inch are hidden. Default is
false.
The rest of the service table entries that restrict existing functionality in the Export dialog come in positive
and negative forms; that is, you can list the features to be included, or you can list the features to be
excluded. For each such pair, you can provide only one of the entries, not both. If you provide neither, all
default elements in that category appear.
For example, you can choose which of the built-in sections to display in the dialog. If you use the positive
form, you list the sections to be shown:
showSections = { 'fileNaming', 'imageSettings' },
This causes the File Naming and Image Sizing sections to be visible, and hides all of the other built-in
sections. If you use negative form, you list the sections to be hidden. For example, this hides the Export
Location section, and shows all other built-in sections:
hideSections = { 'exportLocation' },
These are the service-table entry pairs for each type of restriction:
➤ When you hide a section, all of the preset values set in that section are excluded
from any presets that the user creates when your plug-in is activated.
➤ If you hide the exportLocation section (the topmost section in the default
dialog), Lightroom renders the photos into a temporary location, then deletes that
directory and its contents after your processRenderedPhotos function
terminates.
NOTE: In the Lightroom 1.3 SDK, there was an additional option, postProcessing,
which has been removed in Lightroom 2.0. This section is now only available with the
built-in "Export to Files on Disk" service. If specified, it is ignored by Lightroom 2.0.
➤ Use the function-context object to define cleanup handlers for this call.
➤ Use the export-context object to gain access to the setting chosen by the user (in
exportContext.propertyTable), and the list of photos to be exported.
The function that you define typically contains a loop of this form:
for i, rendition in exportContext:renditions() do
-- Wait until Lightroom has finished rendering this photo.
local success, pathOrMessage = rendition:waitForRender()
CHAPTER 2: Writing a Lightroom Plug-in Customizing the export destination 51
➤ Lightroom renders the photos in a separate background thread, so it is likely that your upload
processing will overlap subsequent rendering operations to some extent.
To declare a Metadata Provider, include an LrMetadataProvider entry in the Info.lua file; for example:
return {
LrSdkVersion = 2.0,
LrToolkitIdentifier = 'com.adobe.lightroom.metadata.sample',
LrPluginName = LOC "$$$/CustomMetadata/PluginName=Metadata Sample",
LrMetadataProvider = 'SampleMetadataDefinition.lua',
}
The information file that declares a Metadata Provider can also declare metadata tagsets (see “Adding
custom metadata tagsets” on page 59), export services and/or filters, but need not do so.
In the current implementation, custom metadata defined by a plug-in has these limitations, which will be
addressed in future versions of the Lightroom SDK:
➤ Values stored in custom metadata fields are stored only in Lightroom's database. In the current
release, a plug-in cannot link custom metadata fields to XMP values or save them with the image file.
➤ A plug-in cannot specify complex data types. You can define simple fields per photo, but you cannot
define a whole spreadsheet per photo.
Each of the entries in the metadataFieldsForPhotos array is a table that describes one metadata field;
each metadata field describes a photo in the catalog. Each field can have only one value per photo. The
following entries are recognized within each table:
id string Required. A unique identifier that allows a plug-in to access this field. The
name must conform to the same naming conventions as Lua variables; that is,
it must start with a letter, followed by letters or numbers, case is significant.
version number Optional. If present, defines a version number specifically for this field, distinct
from the version number defined by schemaVersion in the outer metadata
definition script.
If this item is omitted, the field does not appear in the Metadata panel. This
can be useful for storing private, per-image plug-in information, such as the
image’s ID at an on-line service that is the export destination, or other
cross-reference information.
dataType string Optional. If this field is present, Lightroom disallows any other data type from
being stored in this field. Nil is always permitted. You cannot require that a
field have a value.
values table Required when dataType = "enum", otherwise disallowed. An array of allowed
values. Each entry in the array is a table that must contain a value and a title.
The title is shown in the popup menu; the corresponding value (which must
be a string, number, or Boolean, or nil) is written to the database. The values
table can have only one entry where value = nil. If such an entry is present,
the corresponding label is used when no value has been assigned to this
property for a photo.
➤ If present, your plug-in can store values outside of the enumerated values
in this field.
➤ If not, an attempt to set such a value triggers a Lua error and does not
change the value stored in the database.
readOnly Boolean Optional. Use only when title is provided. When true, the field is visible in the
Metadata panel, but not editable by the user. The value can still be set
programmatically, using LrPhoto:setPropertyForPlugin().
searchable Boolean Optional. Use only when title is provided. When true, this field is stored in a
separate table and indexed for faster searching; this also means that the field
can be chosen by a user as a search criterion for smart collections. Strings
stored in this field must not exceed 511 bytes. Default is false.
browsable Boolean Optional. Use only when title is provided and searchable is true. When
true, this field can be used as a filter in the Library metadata browser.
This sample Metadata Provider script defines three metadata fields of representative types.
return {
metadataFieldsForPhotos = {
{
id = 'siteId',
-- This field is not available in the metadata browser because
-- it does not have a title field. You might use a field like this
-- to store a photo ID from an external database or web service.
},
{
id = 'randomString',
title = LOC "$$$/Sample/Fields/RandomString=Random String",
dataType = 'string', -- Specifies the data type for this field.
},
{
id = 'modelRelease',
title = LOC "$$$/Sample/Fields/ModelRelease=Model Release",
dataType = 'enum',
values = {
{
value = nil,
CHAPTER 2: Writing a Lightroom Plug-in Adding custom metadata 58
},
schemaVersion = 1,
-- must be a number, preferably a positive integer
catalog:assertHasPrivateWriteAccess(
"SampleMetadataDefinition.updateFromEarlierSchemaVersion" )
local myPluginId = 'com.adobe.lightroom.metadata.sample'
if previousSchemaVersion == 1 then
local photosToMigrate = catalog:findPhotosWithProperty( myPluginId,'siteId')
-- optional: can add property version number here
for i, photo in ipairs( photosToMigrate ) do
local oldSiteId = photo:getPropertyForPlugin( myPluginId, 'siteId' )
-- add property version here if used above
local newSiteId = "new:" .. oldSiteId
-- replace this with whatever data transformation you need to do
photo:setPropertyForPlugin( _PLUGIN, 'siteId', newSiteId )
end
elseif previousSchemaVersion == 2 then
-- and so on...
end
end,
}
This is how these fields appear in the Metadata panel, when the user has chosen a metadata tagset that
contains them, or one of the default metadata tagsets "All" or "All Plug-in Metadata" (see “Adding custom
metadata tagsets” on page 59). The user-visible custom fields from plug-ins are shown after all of the
built-in metadata fields.
CHAPTER 2: Writing a Lightroom Plug-in Adding custom metadata tagsets 59
➤ Notice that the field "siteId" does not appear in the panel because no title is defined for it; it is an
invisible field, internal to the plug-in.
➤ The field "randomString" appears with the localized title value, "Random String", as the display label.
Because it is a plain string value, it appears as an editable text field.
➤ The field "modelRelease" also appears with the title value, "Model Release", as the display label.
Because it is an enumerated value, clicking it pops up a menu of the allowed values, each shown using
its own localized title value as the display string.
There are predefined tagsets, and you can also create your own. Your plug-in can define a named
metadata tagset, which can include fields defined by your plug-in, by other plug-ins, or by Lightroom.
This is the Info.lua file for a minimal plug-in that defines a tagset:
return {
LrSdkVersion = 2.0,
LrToolkitIdentifier = 'com.adobe.lightroom.metadata.sample',
LrPluginName = LOC "$$$/CustomMetadata/PluginName=Metadata Sample",
LrMetadataTagsetFactory = 'SampleTagset.lua',
}
The metadata-tagset provider can appear in the same plug-in with export-service and export-filter
providers, and with simple Metadata Providers.
The metadata-tagset provider is a Lua file that returns a tagset definition. You can use the
LrMetadataTagsetFactory entry to specify more than one such file in a single plug-in. For example:
id string Required. An identifier for this tagset that is unique within this plug-in. The
name must conform to the same naming conventions as Lua variables; that is,
it must start with a letter, followed by letters or numbers. Case is significant.
title string Required. The localizable display name of the tagset, which appears in the
popup menu for the Metadata panel.
items table Required. An array of metadata fields that appear in this tagset, in order of
appearance.
Each entry in the items array identifies a field to be included in the Metadata menu. It can be a simple
string specifying the field name, or an array that specifies the field name and additional information about
that field:
fieldname string The first element in the array is the unique identifying name of the field,
or one of the special values described below.
height_in_lines number Optional. For text-entry fields, the number of lines of text for the field.
label string Optional. When the field name is the special value 'com.adobe.label',
this is the localizable string to use as the section label.
➤ Identify built-in metadata fields known to Lightroom with the prefix 'com.adobe.' and the key name,
as defined for use with LrPhoto:getFormattedMetadata(). This form is shown in the example
(“Custom metadata tagset example” on page 61).
➤ Identify fields from your plug-in, or any other plug-in, with the single string "pluginID.field name".
For example, if the plug-in ID is "com.mycompany.uploader" and the field name is "modelRelease",
use the string "com.mycompany.uploader.modelRelease".
➤ You can include all visible metadata from a plug-in by specifying the field name with the wild-card
character "*"; for example, "com.mycompany.uploader.*". The visible fields are included in the
sequence in which they are defined in the definition script. The fields for each plug-in are preceded by
a dividing line and the plug-in’s name.
If you refer to a plug-in that is missing or that defines no visible metadata, it is not an error; the block
and separator for that plug-in are simply not displayed.
➤ You can include all visible metadata from a plug-in by specifying the special field name
"com.adobe.allPluginMetadata". This is the field name used by the built-in "All Plug-in Metadata"
preset.
➤ The special name 'com.adobe.separator' inserts a dividing line in the Metadata panel before the first
field from this plug-in.
CHAPTER 2: Writing a Lightroom Plug-in Searching for photos by metadata values 61
➤ The special name 'com.adobe.label' inserts a section label in the Metadata panel, specified by a
label entry in an array with this name. A label is typically used below a separator.
items = {
'com.adobe.filename',
'com.adobe.copyname',
'com.adobe.folder',
'com.adobe.separator',
'com.adobe.title',
{ 'com.adobe.caption', height_in_lines = 3 },
'com.adobe.separator',
{ 'com.adobe.label', label = LOC "$$$/Metadata/SampleLabel=Section Label" },
'com.adobe.dateCreated',
'com.adobe.location',
'com.adobe.city',
'com.adobe.state',
'com.adobe.country',
'com.adobe.isoCountryCode',
'com.adobe.GPS',
'com.adobe.GPSAltitude',
'com.adobe.lightroom.metadata.sample.randomString',
},
}
},
}
catalog:withReadAccessDo( function()
end )
end )
This simple usage is straightforward, although the function allows many matching operations, depending
on the datatype of the metadata field to be considered.
criteria The allowed values for criteria correspond to choices in the Edit Smart Collection dialog:
rating (number)
pick (enum) Value must be one of: 1 (flagged), 0 (unflagged), -1 (rejected)
labelColor (enum) Value must be one of: 1 (red), 2 (yellow), 3 (green), 4 (blue), 5 (purple),
"custom" (any label not currently assigned to a color), "none"
labelText (string, can be empty) User-assigned name of color label
folder (string) Name of folder, including all parent folders shown in the Folders panel
collection (string) Name of any collection containing this photo
all (string) Any searchable text
filename (string)
copyname (string, can be empty) Copy Name assigned in Metadata panel
fileFormat (enum) Value must be one of: "DNG", "RAW", "JPG", "TIFF", "PSD"
metadata (string) Any searchable metadata
title (string, can be empty)
caption (string, can be empty)
keywords (string, plural, can be empty)
iptc (string) Any IPTC metadata; that is, any text in a field that is indexed by Lightroom.
exif (string) Any EXIF metadata; that is, any text in a field that is indexed by Lightroom.
captureTime (date)
touchTime (date) Edit Date
camera (string, with exact match)
cameraSN (string, with exact match) Camera Serial Number
lens (string, with exact match)
CHAPTER 2: Writing a Lightroom Plug-in Searching for photos by metadata values 63
isoSpeedRating (number)
hasGPSData (Boolean)
country (string, with exact match)
state (string, with exact match)
city (string, with exact match)
location (string, with exact match)
city (string, with exact match)
location (string, with exact match)
creator (string, with exact match)
jobIdentifier (string, with exact match)
copyrightState (enum) Value must be one of: true (Boolean, copyrighted), false (Boolean, public
domain), "unknown"
hasAdjustments (Boolean)
You can search plug-in defined fields, using these special criteria values:
operation The allowed values for operation depend on value type of the criteria field, and also correspond to
selectable values the Edit Smart Collections dialog.
any "contains"
all "contains all"
words "contains words"
noneOf "does not contain"
beginsWith "starts with"
CHAPTER 2: Writing a Lightroom Plug-in Searching for photos by metadata values 64
== "is"
!= "is not"
> is greater than"
< "is less than"
>= "is greater than or equal to"
<= "is less than or equal to"
in "is in range"
== "is"
!= "is not"
> "is after"
< "is before"
inLast "is in the last"
notInLast "is not in the last"
value The value to match against must be of the type indicated for the criteria. Additional parameters value2
and value_unit are used with specific types and operations, as mentioned above.
catalog:withReadAccessDo( function()
end )
end )
A combine entry is followed by an array of elements to be combined. This array can contain nested
combine entries, so the search can become quite complex. For example:
CHAPTER 2: Writing a Lightroom Plug-in Searching for photos by metadata values 66
{
combine = "union",
{
combine = "intersect",
{
criteria = "rating",
operation = ">=",
value = 1,
},
{
criteria = "labelColor",
operation = "==",
value = 1,
},
},
{
criteria = "rating",
operation = "==",
value = 5,
},
}
2. Right click the resulting collection in the Collections panel and choose "Export Smart Collection
Settings."
CHAPTER 2: Writing a Lightroom Plug-in Searching for photos by metadata values 67
5. Edit the resulting code to change value to searchDesc, and include it in your call to findPhotos().
6. Make any other appropriate changes in the code. In this example, for instance, you would not need
the combine element. If you remove it, you can also promote the parameter table to the top level in
searchDesc:
searchDesc = {
criteria = "captureTime",
operation = ">",
value = "2008-02-12",
}
3 Creating a User Interface for Your Plug-in
You can define a user interface to your plug-in with these tools:
➤ Your plug-in can define one or more custom sections to be displayed in the Plug-in Manager dialog or
Export dialog, above and/or below the Lightroom standard sections. The custom sections are
displayed when the user chooses your export destination. You define the UI elements of a custom
section using LrView objects; see “Adding custom dialog views” on page 68.
➤ You can call the functions of the LrDialog namespace to display messages, prompts, and errors to
users in predefined dialogs. See “Displaying predefined dialog boxes” on page 69.
➤ You can use the functions in the LrDialog and LrView namespaces to create your own dialog boxes.
You can display them when users choose your custom menu items, invoke them from tasks, or invoke
them in response to selections in controls you have added to the Export dialog. See “Creating custom
dialog boxes” on page 70.
The LrView class models a node tree, where each node is a UI element, represented by a specific type of
LrView object. A node can be a container or parent of other nodes, or a control, an individual UI element
such as a checkbox, which displays a value and can allow user input. Containers and controls are arranged
in a node tree, or view hierarchy. A view hierarchy has a top-level container node, additional child
containers if needed, and leaf nodes that are the controls.
The LrView namespace and class provides a full set of interface elements, with functionality to layout and
localize the display, and a binding mechanism that lets you tie the displayed values to your plug-in data
and settings.
➤ “User interface elements” on page 70 introduces the UI elements you can create with LrView.
➤ “Binding UI values to data values” on page 79 explains the binding mechanism, with examples of how
to create various relationships between your data and your display.
➤ ““Determining layout” on page 92 explains the placement options and gives examples of various
layout techniques.
➤ The function that you define here is slightly different for the two dialogs; see “Adding custom sections
to the Plug-in Manager” on page 28 and “Adding custom dialog sections to the Export dialog” on
page 43.
➤ For an Export Filter Provider, a very similar function, sectionForFilterInDialog, creates only one
section, rather than multiple sections. See “Defining a post-process action” on page 33.
In any case, however, the function must define the UI to be displayed when each dialog-box section is
expanded. To do so, use the viewFactory object to construct all of the elements of a view hierarchy.
68
CHAPTER 3: Creating a User Interface for Your Plug-in Using dialog boxes 69
To create the containment hierarchy, use the view factory to create a container, and within that call, use it
to create the child containers and controls:
viewFactory:group_box {
...initial property settings...
viewFactory:row {-- a row of controls within the box
...initial property settings...
viewFactory:static_text { -- a text label, contained in the row
...initial property settings...
viewFactory:button { -- a button that responds to a click, contained in the row
...initial property settings...
...
Control nodes have properties that define a tooltip for the node, control the visibility, and affect the size,
displayed font, and enabled state. Additional properties apply to controls of specific types; for instance, a
pop-up menu has an items property, which contains a table of the selectable menu items to display. Each
item is in turn a table containing a title (displayed string) and value (the value returned when the item is
selected):
viewFactory:popup_menu {
title = "myPopup",
items = { -- the menu items
{ title = "First item", value = 1 },
{ title = "Second item", value = 2 },
},
value = LrView.bind( "myPopup_value" ), -- the control value
size = 'small'
},
➤ The types of containers and controls and their view properties are listed and described in “User
interface elements” on page 70.
➤ Certain properties describe node layout; that is, the sizing and placement of each node with respect to
its container and sibling nodes. You can set layout values individually, or use LrView functions to set
spacing and margin values for an entire node tree. The layout properties and functions are described
in “Determining layout” on page 92.
➤ Display strings in all containers and controls (generally specified in the title property) can be
localized to different languages by using the LOC function to specify the string value; for details, see
Chapter 5, “Using ZStrings for Localization.”
➤ Messages
Message dialogs display your text message to the user. They have a single OK button that dismisses
the dialog; you can specify the button text. One version has a "Don’t show again" checkbox, so that
the user can prevent this message from being displayed next time the same situation occurs.
CHAPTER 3: Creating a User Interface for Your Plug-in User interface elements 70
In addition to your text message, these dialogs have configurable OK and Cancel buttons. These
return different values to the invocation function, which you use to decide on the action to be taken.
Again, there is a "Don’t show again" version.
These dialogs are extensible; you can define an optional third button, or a small UI section that you
define using LrView; see “Creating custom dialog boxes” on page 70.
➤ Errors
You can display a simple error message with a single OK button, or you can wrap an error dialog
around a function context, so that if the wrapped function throws an error, the dialog appears. See
“Using function contexts for error handling” on page 15.
You can bring up the platform-defined file-selection dialogs, so that the user can choose a file system
location.
Most of the contents of this dialog are defined by an LrView hierarchy that you define. To build the
contents of a custom dialog, obtain a factory object using the LrView namespace function
LrView.osFactory(). Like the confirmation dialogs, this dialog automatically contains configurable OK
and Cancel buttons.
You can choose to make this dialog user-resizeable, and can also choose to save its most recent frame size
as one of your plug-in settings. The location of the dialog is also saved, if the user moves it.
The example code in “Building a basic dialog” on page 95 demonstrates how to build and invoke a custom
dialog within a function context.
Containers
When creating a dialog or a section for the Plug-in Manager or Export dialog, you generally begin with a
top-level container, then, within that container, create its children. Depending on the complexity of your
interface, the children can be nested containers (such as a tabbed view that contains tabbed pages),
placement containers (rows and columns), or the visible controls (such as text and buttons).
➤ All containers have the shared view properties listed in “General view properties” on page 76, except
as mentioned.
➤ All containers except spacer have the layout properties listed in “Determining layout” on page 92.
CHAPTER 3: Creating a User Interface for Your Plug-in User interface elements 71
spacer This is a row that contains no child nodes. width, height: The size in pixels.
It is used only for spacing.
Controls
You can use the LrView factory to create visible controls of types common to Windows and Mac OS
interface systems. If the creation function is called within the creation of a container, the control is a child
of that container.
For complete details of how to create the controls and specify their appearance and behavior, see the
Lightroom SDK API Reference. The following table summarizes the available control types and lists their
type-specific properties.
➤ All controls have the shared view properties listed in “General view properties” on page 76 and
“Control node view properties” on page 77.
CHAPTER 3: Creating a User Interface for Your Plug-in User interface elements 72
See “Binding combo box selections” on All edit and text properties. See
page 86. “Edit-field view properties” on page 77
and “Text view properties” on page 79.
edit_field An editable text field. An edit field accepts All edit and text properties. See
keyboard input when it has the input focus. “Edit-field view properties” on page 77
and “Text view properties” on page 79.
User input is committed (that is, the value
is updated) with every keystroke if
immediate is true. If immediate is false,
input is committed when the control loses
focus. There is a platform difference in the
focus behavior:
The following figures show examples of the various control and container types. The appearance is
appropriate to the platform; these examples show some of each.
Containers
and
placement
controls
CHAPTER 3: Creating a User Interface for Your Plug-in User interface elements 75
Buttons.
selection,
edit and
text
controls
Other
controls
CHAPTER 3: Creating a User Interface for Your Plug-in User interface elements 76
View properties
Properties in container and control nodes affect the layout of the controls, and their appearance. Layout
properties, and certain view properties, are available to all nodes, both containers and controls. Other view
properties are available only in control nodes. Most types of controls have additional view properties
specific to their type; these are reflected in the creation parameters.
Of the properties that are available in both containers and controls, many are connected with layout
behavior; these are discussed separately in “Determining layout” on page 92. The following view
properties are available in all containers and controls except the layout containers, row and column:
Default nil.
visible Boolean Determines whether a container or control is shown or
hidden. This is not the same as being enabled or disabled;
the disabled state is only applied when a control is visible.
Default is true.
These properties are available in control nodes of all types, but not in containers.
One of:
regular (the default)
small
mini
These properties are available in control nodes that contain editable text; these include edit_field,
combo_box, and password_field.
These properties apply to any control that displays text, including popup_menu, static_text, and
push_button, as well as the editable text controls.
When you create a binding, the value or state of the UI element reflects the data value, and the data value
reflects the UI element state. This is a two-way relationship; when the binding is established, the data value
from the table is pushed to the view, and when the user changes the bound value in the view (by selecting
a checkbox, for instance, or entering a value in a text field), the table is notified and the corresponding
data key value or values change accordingly. Similarly, when your program changes a value in the table,
the bound UI elements are updated to display the new value.
To create bindings:
1. Specify a bound table at some level of the view hierarchy. Set the table as the value of the
bind_to_object property (you can also use the name object for this property). The bound table of a
parent container is inherited by its children, but can be overridden.
➣ When you create a dialog box, you must set the bound table explicitly.
➣ When you create a section for the Plug-in Manager or Export dialog using the
sectionsForTopOfDialog or sectionsForBottomOfDialog functions, the settings table for
your plug-in is passed to those functions as the propertyTable value. This table contains both
export settings that you have defined for your plug-in (see “Remembering user choices” on
page 46) and Lightroom-defined export settings (see “Lightroom built-in property keys” on
page 51).
The propertyTable is automatically set as the default bound table for all of the UI elements in the
view hierarchy for that section. However, the bindable synopsis for the section is not part of the
view hierarchy; if you want to make that value dynamic, you must specify the table explicitly. See
“Adding custom sections to the Plug-in Manager” on page 28.
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 80
2. For each specific UI element, set the value of each dynamic property using the LrView.bind()
function to associate that value with a specific key in the bound table.
➣ The simplest binding simply mirrors the key value and the property value; for instance, setting one
value to true sets the other value to true.
➣ You can use the LrBindings functions to create other common mappings between the bound
key value and the view property value. See “Specifying bindings” on page 80.
➣ For more complex mappings, see “Transforming values” on page 88 and “Binding multiple keys”
on page 90.
A typical example is a binding between the visible property and a particular settings value, so that a
control is only shown when the appropriate setting is present. For example, in the File Settings section of
the Export dialog, the control that appears next to the Format combo box changes according to the
selected format.
When JPEG is selected, there is a slider for setting the Quality. When you select the TIFF format, the slider is
hidden and a Compression pop-up menu is shown. For PSD and DNG, both of these controls (and their
labels) are hidden.
To accomplish this, Lightroom binds the Format pop-up menu’s value property to the LR_format setting.
Then the visible property of the slider and its label are bound to the JPEG value of LR_format. The
example code in “Changing the contents of a view dynamically” on page 97 shows how to use bindings in
this way, setting the visibility state of different sets of controls, based on the selection in a pop-up menu.
Notice in this example that two control values are related by being bound to the same key value; this is
how you bind control values to one another.
NOTE: Bindings are used to create dynamic text in LrView objects only. The title of a dialog box, for
example, is not part of an LrView object, and you cannot bind it. Similarly, the title of an Export dialog
section cannot be bound.
Specifying bindings
The LrView namespace function LrView.bind() creates a direct association between a key or keys in an
observable table and a view property value. Use this function when creating the view or control, to specify
the view property value. For example:
visible = LrView.bind( "LR_export_useSubfolder" )
You can then use the shortcut to specify dynamic property values:
...
viewFactory:static_text {
title = bind 'mySetting',
...
The required argument of LrView.bind() is the key name; by default, this is in the table that is already
bound to the UI element; that is, the value of bind_to_object in the same UI element. This is inherited in
the view hierarchy, but can be overridden at any level.
You can override the bound table for a specific binding by passing the LrView.bind() function a table
containing both the key and the table it comes from:
visible = LrView.bind { key = "mySetting", bind_to_object = "myTable" }
This allows you to bind different properties in one view object to keys in different tables.
The bound table is typically the export-settings table, since your UI is typically a way for your user to see
and set these values. The SDK makes this default case easy for sections that you define for the Export
dialog. In views created with sectionsForTopOfDialog and sectionsForBottomOfDialog, the value of
bind_to_object for the entire view hierarchy is set automatically to the export-settings table passed
along with the view factory. See “Adding custom sections to the Plug-in Manager” on page 28.
Simple bindings
The simplest binding is between a property in the LrView object and a settings key of the same datatype,
and simply keeps the two synchronized. For example:
visible = LrView.bind( "LR_export_useSubfolder" )
In this case, both the local property (visible) and the bound table item (a Lightroom-defined export
setting) have Boolean values. Setting the use-subfolder preference to true (in the Export dialog, for
instance) makes the control visible.
For some other common types of binding, you can use an LrBinding function as the value assignment; for
example:
visible = LrBinding.negativeOfKey( "LR_export_useSubfolder" )
This binds the property to the opposite of the table value; that is, setting the use-subfolder preference to
true hides the control. The binding works in both directions; that is, hiding the control would also set
LR_export_useSubfolder to true. This function can be used to negate numeric as well as Boolean values;
for example, a value of 2 would become -2.
Although negativeOfKey() works both ways, and with numeric values, the other LrBinding functions
can be used only with Boolean values, and work only in one direction; a change in the bound table sets the
bound property value, but not the reverse. The LrBinding functions allow you to:
➤ Set a Boolean property to the opposite of a Boolean key value, or a numeric property to the negation
of a numeric key value (LrBinding.negativeOfKey).
➤ Set a Boolean property when a key value is or is not present (LrBinding.keyIsNil, keyIsNotNil).
➤ Set a Boolean property when a key value is or is not equal to a specific value (LrBinding.keyEquals,
keyIsNot).
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 82
➤ Set a Boolean property when a set of Boolean keys are either all true, or when any one is true
(LrBinding.andAllKeys, orAllKeys); for more information on how this works, see “Binding multiple
keys” on page 90.
For details of the LrBinding functions, see the Lightroom SDK API Reference.
The LrView objects that define your UI elements in an Export dialog section are automatically registered
as observers of the export-settings table that is passed on creation; see “Adding custom sections to the
Plug-in Manager” on page 28.
To use export settings in another context, or to define additional program data, use the function
LrBindings.makePropertyTable() to create an observable table, and populate it with your own plug-in
settings or any other program data.
An observable table must be created with a function context, so that Lightroom can clean up the
notifications if anything goes wrong. (See “Using function contexts for error handling” on page 15.) A
function context is created using LrFunctionContext.callWithContext(). This passes a
function-context object to its main function; you pass that object on to your table-creation function. For
example:
LrFunctionContext.callWithContext("showCustomDialog", function( context )
local properties = LrBinding.makePropertyTable( context )
properties.url = "http://www.adobe.com" -- create a settings value
-- add code to take create dialog contents
end)
When you create a new table, it is initially empty. You can explicitly add keys and values, as in the example.
However, it is not necessary to add a key to a table before you reference it in a binding; if it is not yet in the
table, its value is nil. The example in “Transforming values” on page 88 shows how a control’s value is
bound to a key that is not yet in the table. When the control first gets a value, the key is put into the table
with that value.
TIP: You can use a naming convention to distinguish program data from persistent export settings (that is,
those specified in the exportPresetFields table; see “Remembering user choices” on page 46). For
example, you might use an underscore prefix, "_tempUrl," to indicate a local data property.
You can create a general and flexible response to a change in an observable table by adding an observer.
An observer associates a function that you define with a key in the table, so that whenever the key value
changes, the function is called.
To receive notification of changes in the table you create, use this function to register an observer of the
table:
propertyTable:addObserver( key, func )
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 83
For example:
LrFunctionContext.callWithContext("showCustomDialog", function( context )
local myPropTable = LrBinding.makePropertyTable( context )
mypropTable:addObserver( 'mySetting', function( properties, key, newValue )
-- do something when this value changes
end )
-- add code to create dialog contents
end)
The handler function you specify for your observer takes as arguments the observed table (so you can
access other data values), the key whose value change triggered the notification (in case you are using the
same handler function for multiple keys), and the new value of that key.
You can define a function to handle more than one key notification, using the key argument to distinguish
which key changed. If you do, you must pass the function to a separate addObserver() call for each key.
For examples of how and why to add an observer to a table, see “Binding combo box selections” on
page 86, and Chapter 7, “Getting Started: A Tutorial Example.
The value property of a radio button or checkbox controls and reflects the current selection state:
➤ In both, if the user checks the button, the checked_value becomes the new control value.
➤ In the checkbox, if the user unchecks the button, the unchecked_value becomes the new control
value.
f:checkbox {
title = "Value will be string",
value = bind 'checkbox_state', -- bind to the key value
checked_value = 'checked', -- this is the initial state
unchecked_value = 'unchecked', -- when the user unchecks the box,
-- this becomes the value, and thus
-- the bound key value as well.
},
f:static_text {
fill_horizontal = 1,
title = bind 'checkbox_state', -- bound to same key as checkbox value
},
},
-- (add radio button container here for example 2)
-- (add pop-up container here for example 3)
local result = LrDialogs.presentModalDialog( -- invoke the dialog
{
title = "Binding Buttons Example",
contents = contents,
}
)
end )
The user cannot uncheck a radio button; a selected button is deselected only when another button in the
group is selected. Simply putting the buttons in the same container does not enforce this usage; to
arrange it, bind the value of each button in the set to a different value of the same key.
EXAMPLE 2: Add this to the previous code for an example of binding in a set of radio buttons:
f:group_box { -- the buttons in this container make a set
title = "Radio Buttons",
fill_horizontal = 1,
spacing = f:control_spacing(),
f:radio_button {
title = "Value 1",
value = bind 'my_value', -- all of the buttons bound to the same key
checked_value = 'value_1',
},
f:radio_button {
title = "Value 2",
value = bind 'my_value',
checked_value = 'value_2',
},
f:radio_button {
title = "Value 3",
value = bind 'my_value',
checked_value = 'value_3',
},
f:static_text {
fill_horizontal = 1,
title = bind 'my_value',
},
},
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 85
The pop-up menu and the menu component of a combo box allow you to specify a set of choices, using an
items table; each item entry is a table containing a title and value. The title is localizable display text,
that appears in the menu (see Chapter 5, “Using ZStrings for Localization").
items = { { title = "First item", value = 1 },
{ title = "Second item", value = 2 },
{ title = "Third item", value = 3 }, },
The value of the item that the user selects from the menu becomes the control’s value. For the pop-up
menu, the title becomes the control’s title, and is displayed in the control when the menu is not
shown. (For the combo box, the displayed text is the value, or the result of the value_to_string
function; see “Edit-field view properties” on page 77.)
EXAMPLE 3: This code fragment adds a pop-up menu to the previous example, with the currently selected
value from the menu similarly bound to a static text value:
f:group_box {
title = "Popup Menu",
fill_horizontal = 1,
spacing = f:control_spacing(),
f:popup_menu {
value = bind 'my_value', -- current value bound to same key as static text
items = { -- the menu items and their values
{ title = "Value 1", value = 'value_1' },
{ title = "Value 2", value = 'value_2' },
{ title = "Value 3", value = 'value_3' },
}
},
f:static_text {
fill_horizontal = 1,
title = bind 'my_value', -- bound to same key as current selection
},
},
}
You can bind the items property to a settings key to create a dynamic menu. However, you can only set
the whole menu at once; you cannot bind individual item values.
EXAMPLE 4: This code binds the currently selected value from a pop-up menu to the same key as an
editable text value. The user can change this value by entering any text in the edit field; the entered text
shows up immediately as the value of the pop-up control.
However, since the user can enter any text, that text might not match the menu items. This code shows
how to use the pop-up control’s value_equal function to do a case-insensitive comparison of the
user-entered value with the item values. The function is called for each item until it returns true, or has
gone through all the items.
➤ If the entered text matches one of the item values (that is, the function returns true), the matching
item becomes the selected item in the pop-up menu, and the item’s title text is displayed in the
pop-up control.
➤ If the function goes through all the items without finding a match, the pop-up control shows no
selection; that is, it appears blank, and the next time the user pops up the menu, none of the items is in
the selected state. The entered value remains in the pop-up control’s value property.
local LrDialogs = import "LrDialogs"
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 86
For a combo box, the user can enter text in the edit-field portion, which becomes the new control value. If
you select an item from the menu portion, that item value becomes the control value; this provides an
input shortcut for the user. Unlike the pop-up menu, the combo box menu items are simple values; if you
need to localize them, you must do so when building the item array.
This example shows how to create a dynamic menu for a combo box that gives previously-entered values
as menu choices. This code:
➤ Binds the value and items of the combo box to data properties storeValue and storeItems.
➤ Creates an observer for the storeValue property, so that a change in that property (caused by
entering a new value in the combo box) calls a function.
➣ The observer function checks to see if the current value is already in the items list (stored in
storeItems), and if it is not, adds it to the list.
➣ Because of the binding, any change the function makes to the storeItems property is
automatically reflected in the combo box items.
local LrBinding = import "LrBinding"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrStringUtils = import "LrStringUtils"
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 87
Complex bindings
The LrBinding functions provide a particular, limited set of value transformations. To specify more
complex bindings, the argument to LrView.bind() can be a table with these items:
key A key name in the bound table. The value can be mapped to a value for the
local property by the transform function.
bind_to_object Optional. The name of an observable table which overrides the value of the
—or— bind_to_object view property.
object
transform Optional. A function that maps the key or key values to the local property
value. See “Transforming values” on page 88. This function is called
immediately when the value changes in either the bound view property or
the bound table key.
Here is an example of binding to keys in two different tables in a single view object:
...
visible = LrView.bind("myBooleanSetting"), -- simple binding between two
-- Booleans in the default table
enabled = LrView.bind( {
key = "mySetting", -- a single key
bind_to_object = mySettingsTable, -- a non-default bound table
transform = function( value, fromTable ) -- a mapping function
...
end
}
)
...
Transforming values
The transformation function that you specify for a binding maps the value of a key in the bound table to a
value in the bound property. If the LrBinding functions do not provide mapping that you need, define
your own transformation function. It is passed these parameters:
value: The new value of the key or property that changed.
fromTable (Boolean): True if the change that triggered this notification was in the bound table, false if
the change was in the bound view property.
Your function should return the new value for the destination property or key.
This simple example creates a slider with a range of 0-110, then reports when the value goes over 100, by
using a transformation function. The slider value and the visible property of a text box are bound to the
same key. For the text box, the transform function returns true (making visible true) only when the value
is over 100.
sectionsForTopOfDialog = function(viewFactory, propertyTable)
return {
{
title = "Section Title",
viewFactory:slider {
min = 0,
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 89
max = 110,
value = LrView.bind "slider_value",
title = "slider title",
},
viewFactory:static_text {
title = "You’re over a hundred",
visible = LrView.bind {
key = "slider_value",
transform = function(value, fromTable)
return value > 100
end
}
}
}
}
end
Transformations can work in both directions; changes in the bound property affect the bound table key,
and changes in the table key affect the property. If you write a custom function for a one-way
transformation, return the value LrBinding.kUnsupportedDirection to indicate that one or the other
direction is not supported by your transformation.
Here is an example of a one-way transformation. This example shows a transformation that makes a text
display visible only when text is entered in an edit field. The transform function checks for a value of nil or
the empty string in the key to which both controls are bound. This example pops up a dialog, so it needs to
create an observable table to hold the data.
local LrBinding = import "LrBinding"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrView = import "LrView"
LrFunctionContext.callWithContext( 'bindingExample', function( context )
local f = LrView.osFactory() -- obtain the view factory
local properties = LrBinding.makePropertyTable( context ) -- make a settings table
-- the new table is initially empty
local contents = f:column {-- create view hierarchy for dialog
spacing = f:control_spacing(),
bind_to_object = properties, -- default bound table is the one we made
f:row {
fill_horizonal = 1,
spacing = f:label_spacing(),
f:static_text {
title = "Type anything:",
alignment = 'right',
},
f:edit_field {
fill_horizonal = 1,
width_in_chars = 20,
immediate = true,
value = LrView.bind( 'text' ), -- creates a key 'text'
-- the initial value of the new key is nil
-- setting its value (by entering text in the control)
-- puts it into the table
},
},
f:static_text {
place_horizontal = 0.5,
title = "This is only visible when there is text in the edit field",
visible = LrView.bind {
CHAPTER 3: Creating a User Interface for Your Plug-in Binding UI values to data values 90
end )
To specify even more complex bindings, between a property in a view object and multiple keys in one or
more bound tables, the value part of a binding key-value pair can be a table with these items:
keys A table specifying one or more keys. The table can have these entries:
operation Required. A function defining an operation to perform on the key values; the
result of this operation is passed to the transform function.
This function is called when any specified key value changes. The function
you define receives three parameters:
values: A special look-up table of key-value pairs with the current values
of all specified keys. The key portion of the pair uses the uniqueKey
name, if provided. (This is not a general-purpose table; you cannot
iterate over the values.)
This function is not called immediately, but at the end of an event cycle; this
means that, if the change is in the bound table, more than one key value can
have changed. If changes occur in both directions, the function is called
twice.
transform Optional. A function that maps the return value of the operation function to
the local property value. See “Transforming values” on page 88.
This example shows multiple binding. The dialog contains two edit fields, each with its value bound to a
different key. A static text box below them has its visible property bound to both keys; the operation
makes it true only when both values are equal (meaning that the same text has been typed into both edit
fields, or they are both empty).
local LrBinding = import "LrBinding"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrView = import "LrView"
LrFunctionContext.callWithContext( 'multiBindingExample', function( context )
local f = LrView.osFactory() -- get view factory
local properties = LrBinding.makePropertyTable( context ) -- make empty table
local contents = f:column { -- create view hierarchy
spacing = f:control_spacing(),
bind_to_object = properties, -- default bound table is the one we made
f:row {
fill_horizonal = 1,
spacing = f:label_spacing(),
f:static_text {
title = "Type anything:",
alignment = 'right',
width = LrView.share( 'label_width' ),
},
f:edit_field {
fill_horizonal = 1,
width_in_chars = 20,
immediate = true,
value = LrView.bind( 'text1' ), -- bind to the first key
},
},
f:row {
fill_horizonal = 1,
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 92
spacing = f:label_spacing(),
f:static_text {
title = "Type more:",
alignment = 'right',
width = LrView.share( 'label_width' ),
},
f:edit_field {
fill_horizonal = 1,
width_in_chars = 20,
immediate = true,
value = LrView.bind( 'text2' ), -- bind to the second key
},
},
f:static_text {
place_horizontal = 0.5,
title = "This is only visible when the text in the two fields are equal",
visible = LrView.bind {
keys = { 'text1', 'text2' }, -- bind to both keys
operation = function( binder, values, fromTable )
if fromTable then
return values.text1 == values.text2 -- check that values are ==
end
return LrBinding.kUnsupportedDirection
end,
},
},
}
local result = LrDialogs.presentModalDialog( -- invoke dialog
{
title = "Multi Binding Example",
contents = contents,
}
)
end )
Determining layout
Both the initial layout of a container, and subsequent automatic layout operations, use a set of parameters
set by properties in both the container and its child nodes. These values control the initial layout, and, if
the containing dialog is resizeable, the way the layout changes if the dialog size changes.
➤ Spacing values determine how child nodes are placed relative to one another. See “Relative
placement of sibling nodes” on page 93.
➤ Margin values determine how a node is placed and sized within its parent node. See “Placement
within the parent” on page 93.
➤ You can obtain default layout values using “Factory functions for obtaining layout values” on page 94.
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 93
If any of the child node fill needs cannot be met, they are
given a percentage of the extra space in the proportion
to how much they specified. For instance, if three nodes
specify 0.2, 0.2, and 0.4, and there is not enough extra
space, the nodes get 25%, 25% and 50% of the extra
space that is available.
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 94
Call these functions from the view factory passed to the sectionsForTopOfDialog or
sectionsForBottomOfDialog function, or obtained using the LrView namespace function
LrView.osFactory().
Layout examples
The following examples show how to build a basic dialog with an initial layout, how to make labels line up
properly, and how to set the dialog up to take advantage of automatic layout on resize.
The following code creates a basic dialog within a function context. (See “Using function contexts for error
handling” on page 15.)
➤ It creates a properties table with a plug-in defined property, url, which contains a URL.
➤ It defines the contents of the dialog box using an LrView factory: a label, and an edit field that shows
the property value.
This code demonstrates a very simple layout, where the topmost and only container is a row view, which
uses default values to place its two children, a label and an edit field.
local LrBinding = import "LrBinding"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrHttp = import "LrHttp"
local LrView = import "LrView"
LrFunctionContext.callWithContext( 'dialogExample', function( context )
local f = LrView.osFactory() --obtain a view factory
local properties = LrBinding.makePropertyTable( context ) -- make a table
properties.url = "http://www.adobe.com" -- initialize setting
local contents = f:row { -- create UI elements
spacing = f:label_spacing(),
bind_to_object = properties, -- default bound table is the one we made
f:static_text {
title = "URL",
alignment = 'right',
},
f:edit_field {
fill_horizonal = 1,
width_in_chars = 20,
value = LrView.bind( 'url' ),-- edit field shows settings value
},
}
local result = LrDialogs.presentModalDialog( -- invoke a dialog box
{
title = "Go to a URL",
contents = contents, -- with the UI elements
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 96
Typically, a dialog contains vertical sets of controls and their labels. The following code demonstrates how
make right-aligned labels on the left side of the dialog, with matching left-aligned controls on the right
side.
To make this happen, the example uses the alignment property and the LrView.share() function.
➤ The alignment property determines whether a control is right-aligned, left-aligned, or centered. Since
at least one of these labels is wider than the text it is showing, the labels need to be right-aligned.
Labels should generally be right-aligned in any case, because if the dialog is translated, the size of the
text changes.
➤ The namespace function LrView.share() binds a property value to an identifier that has no value of
its own, but indicates that this property value is to be shared across the hierarchy with other
properties that share the same identifier. In this case, the width of both labels is shared because they
use the same identifier, label_width. When layout occurs, the largest width value of the two labels is
used as the width for both of them.
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrView = import "LrView"
LrFunctionContext.callWithContext( 'bindingExample', function( context )
local f = LrView.osFactory() -- obtain view factory
local contents = f:column { -- define view hierarchy
spacing = f:control_spacing(),
f:row {
spacing = f:label_spacing(),
f:static_text {
title = "Name:",
alignment = "right",
width = LrView.share "label_width", -- the shared binding
},
f:edit_field {
width_in_chars = 20,
},
},
f:row {
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 97
spacing = f:label_spacing(),
f:static_text {
title = "Occupation:",
alignment = "right",
width = LrView.share "label_width", -- the shared binding
},
f:edit_field {
width_in_chars = 20,
},
},
}
local result = LrDialogs.presentModalDialog( -- invoke the dialog
{
title = "Dialog Example",
contents = contents,
}
)
end )
This simple example of dynamic layout shows one set of controls and hides another set, based on the
selected value in a pop-up menu. The dialog contains the popup and three views, each containing an
alternate set of controls. When the user makes a selection in the pop-up menu, one of the views is shown,
and the other two are hidden. For example:
This technique makes use of the overlapping placement style, and demonstrates binding of a property in
one node to a property in another, so that changing one also changes the other.
The overlapping value for the place property causes all of the children of a node to be placed in the
same space. The parent views are made big enough to enclose the largest child in any view, and all of the
children are placed within that space.
If all of the children were visible at the same time, they would display on top of one another. To make sure
only one view is visible at a time, we bind the visible value of each alternative view to a unique value of
the pop-up menu. When the user makes the selection that has this value, the view bound to that value is
shown, and the other views (bound to different values) are hidden.
➤ You only need to set the visibility of the parent view; when the parent is hidden, all of its child nodes
are also hidden, regardless of their individual visibility settings.
➤ The LrBindings.whenKeyEquals() function sets visible to true only when the specified value of
the bound property is set. You could choose to bind the true value to, for example, a logical OR or AND
of several key values.
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 98
This example creates the overlapping views shown in the figure, where the controls shown below the
format pop-up depend on the selection in the pop-up menu.
local LrBinding = import "LrBinding"
local LrDialogs = import "LrDialogs"
local LrFunctionContext = import "LrFunctionContext"
local LrView = import "LrView"
LrFunctionContext.callWithContext( 'bindingExample', function( context )
local f = LrView.osFactory() -- obtain the view factory
local properties = LrBinding.makePropertyTable( context ) -- make settings table
-- add some keys with initial values
properties.format = "jpeg"
properties.jpeg_quality = 80
properties.tiff_compression = "none"
local contents = f:column { -- define the view hierarchy
spacing = f:control_spacing(),
bind_to_object = properties, -- default bound table is the one we made
f:popup_menu {
items = {
{ title = "JPEG", value = "jpeg" },
{ title = "TIFF", value = "tiff" },
},
value = LrView.bind 'format', -- bind selection to the format key
},
f:column { -- place two views in the same space
place = "overlapping",
-- JPEG view
f:view {
-- shown only when format selection is JPEG
visible = LrBinding.keyEquals( "format", "jpeg" ),
margin = 3,
f:row {
spacing = f:label_spacing(),
f:static_text {
title = "Quality:",
},
f:slider {
min = 0,
max = 100,
value = LrView.bind 'jpeg_quality', -- sets a JPEG value
fill_horizontal = 1,
place_vertical = 0.5,
},
f:edit_field {
width_in_digits = 3,
min = 0,
max = 100,
precision = 0,
value = LrView.bind 'jpeg_quality', -- sets a JPEG value
},
},
},
-- TIFF view
f:view {
-- shown only when format selection is TIFF
visible = LrBinding.keyEquals( "format", "tiff" ),
margin = 3,
f:row {
spacing = f:label_spacing(),
f:static_text {
CHAPTER 3: Creating a User Interface for Your Plug-in Determining layout 99
title = "Compression:",
},
f:popup_menu {
items = {
{ title = "None", value = 'none' },
{ title = "LZW", value = 'lzw' },
{ title = "ZIP", value = 'zip' },
},
value = LrView.bind 'tiff_compression',-- sets a TIFF value
},
},
},
},
}
local result = LrDialogs.presentModalDialog( -- invoke the dialog
{
title = "Dialog Example",
contents = contents,
}
)
end )
4 Writing a Web-engine Plug-in
This chapter describes the web-engine plug-in mechanism in the Lightroom SDK. This mechanism allows
you to define new HTML web engines for the Web module. A web engine controls how a photo gallery is
generated.
Web-engine plug-ins use a different architecture from standard plug-ins, and are not managed by the
Plug-in Manager dialog. All available web engines appear in the Engine panel at the upper right of the
Web module of Lightroom, including those predefined by Lightroom and any defined by plug-ins.
Your plug-in can also customize the control panels in the Web module, so that the controls map to
user-customizable features of your own web engine.
➤ A manifest file named manifest.lrweb, which maps LuaPage source files to the HTML output files
that make up a photo gallery. This file uses a special command set; see “Web SDK manifest API” on
page 112.
➤ An information file named galleryInfo.lrweb, which defines the data model and customized UI for
your gallery type. See “Defining the data model” on page 101.
➤ One or more web-page templates, in the form of LuaPages; that is, HTML pages with embedded Lua
code that is evaluated for display in the preview browser, or on publication, to generate dynamic
content. See “LuaPage syntax” on page 118.
➤ Additional resources and supporting files, such as images, style sheets, string dictionaries for
localization, and code files that define special behaviors.
Collect these files into a single folder, which you must place in the following directory according to your
operating system:
IN WINDOWS: LightroomRoot\shared\webengines
The name of the plug-in folder must end with .lrwebengine; for example, myWebPlugin.lrwebengine.
Folder contents
Here are the contents of a sample web-engine folder named default_html.lrwebengine:
Root default_html.lrwebengine/
Template manifest.lrweb
information galleryInfo.lrweb
100
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 101
LuaPage about.html
templates detail.html
foot.html
grid.html
head.html
Resources resources/
JavaScript js/live_update.js
images misc/icon_fullsize.png
shadow-grid.gif
shadow.png
Localization strings/
dictionaries de/TranslatedStrings.txt
en/TranslatedStrings.txt
fr/TranslatedStrings.txt
ja/TranslatedStrings.txt
Top-level property names are predefined, as shown in “GalleryInfo top-level entries” on page 102.
➤ The top-level model property is extensible, containing both predefined and plug-in-defined sections
to create a complex data model. Sections are grouped, using brackets and dot notation to specify a
complex property name. See “Data model entries” on page 103.
➤ The top-level views property is a function that customizes the user interface for your web engine,
creating UI controls in the Web module control panels and binding them to the data model that your
plug-in defines. See “Defining a UI for your model” on page 105.
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 102
title A localizable title string for the gallery type, which appears in the
Web module’s Engine list. You can localize the title string using the
LOC function.
The keys in this table are strings that use dot-separated notation
to break into separate areas; for example,
"model.nonDynamic.numRows".
The About box is displayed when the user chooses Web > About
[thisEngine].
supportsLiveUpdate Boolean, true if this web engine supports the Live Update
mechanism. See “Web HTML Live Update” on page 125.
This example shows the top-level entries from the galleryInfo.lrweb file of the built-in HTML gallery:
return {
LrSdkVersion = 2.0,
LrSdkMinimumVersion = 2.0, -- minimum SDK version required by this plugin
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 103
Here is the About box for the built-in HTML Web Gallery:
"$$$/Templates/HTML/Defaults/props/SiteTitle=Site Title",
...
}
}
Within the predefined photoSizes section, the size-class names (in this example, large and thumb) are
defined by the plug-in. Within each size class, however, there are a set of predefined properties such as
width and height.
Model properties can have simple number, string or color values, but to make a property dynamic, you can
make the value a function definition. See “Creating a dynamic data model” on page 109.
In addition, for each unique name, you must also add an entry
that tells Lightroom what CSS selector to use for that name, in this
form:
[ "appearance.cssClassName.cssID" ] = selectorName
The function returns a table of view descriptions by name, with entries that correspond to the control
panels at the right of the Web module.
labels The Site Info panel, which allows users to specify text to be
associated with the site.
colorPalette The Color Palette panel, which allows users to adjust the colors of
various elements of the site.
appearanceConfiguration The Appearance panel, which allows users to adjust the appearance
of individual photos.
outputSettings The Output Settings panel, which allows users to adjust various
output parameters such as image quality and metadata inclusion.
Plug-in-defined engine
"labels"
"colorPalette"
"appearanceConfiguration"
"outputSettings"
Within each entry, you can use the view factory object to create UI controls. Set the bound table to be the
controller table, and bind control values to data values you have defined in your model.
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 107
Here is an example of the format of the views function for your web engine:
return {
...
views = function( controller, f )
local LrView = import "LrView"
local bind = LrView.bind
local multibind = viewFactory.multibind
return {
labels = f:panel_content { -- returned item identifies panel
bind_to_object = controller, -- bound table is passed controller
f:subdivided_sections {
f:labeled_text_input { -- create controls in the sections
title = "Site Title",
value = bind "metadata.siteTitle.value", -- bind to model data
},
...additional content...
},
colorPalette = f:panel_content {
bind_to_object = controller,
...define content...
},
appearanceConfiguration = f:panel_content {
bind_to_object = controller,
...define content...
},
outputSettings = f:panel_content {
bind_to_object = controller,
...define content...
},
},
}
end,
...
}
Notice that the view factory passed to views function is an extension of the standard view factory
described in Chapter 3, “Creating a User Interface for Your Plug-in." It is an object of type
LrWebViewFactory, and it defines these additional functions for creating UI content suitable to the Web
module (see the Lightroom SDK API documentation for details):
panel_content Creates a top-level panel in the Web module, which contains sections
divided by heavy black lines.
subdivided_sections Creates a section within a panel in the Web module. Within the section,
control rows and columns are separated by light gray lines.
header_section_label Creates a text label for a section within a panel, with suitable formatting.
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 108
content_column These create column-style containers for controls within a section. Some
slider_content_column are generic, and some are specialized to particular types of row content,
checkbox_and_color_row with suitable formatting.
color_content_column
content_section
header_section
The Image Info panel allows the user to specify text for use in gallery pages. Each text label is named with
a label, such as Caption or Title, and can be enabled with a checkbox. A menu of preset values (the custom
settings menu) allows the user to get dynamic text from the current image’s metadata. The presets can be
further customized using the Text Template Editor, which allows users to define text that incorporates
dynamic values from metadata. The user can also save customized text templates as new presets.
Your page templates can access the user’s text choices using this syntax:
You can use the model entry perImageSetting to add text labels to your model. Each setting is identified
by a property name that you define. Each setting adds a row of controls to the Image Info panel, which
allows the user to choose the text value of that label.
Your page templates can access the user’s text choices for it using this syntax:
$image.metadata.propertyName
For example, the following defines a simple per-image description and title:
model = {
...
["perImageSetting.details"] = {
enabled = true,
value = "Default Custom-Text value",
title = LOC "$$$/WPG/HTML/CSS/properties/ImageDetails=Details",
},
["perImageSetting.datatext"] = {
enabled = true,
value = "Default Custom-Text value",
title = LOC "$$$/WPG/HTML/CSS/properties/ImageData=Metadata",
},
}
CHAPTER 4: Writing a Web-engine Plug-in Defining the data model 109
The localized title text appears as the display name for the label. The checkbox and presets menu are
supplied by Lightroom. The value of the value entry appears as the default value for the Custom Text
preset choice.
To incorporate the user’s choice of text in the image-detail template page, use code like this:
<html>
<body>
<lr:ThumbnailGrid>
<lr:GridPhotoCell>
<pre>
$image.metadata.datatext, $image.metadata.details
</pre>
</lr:GridPhotoCell>
<lr:GridRowEnd>
<br>
</lr:GridRowEnd>
</lr:ThumbnailGrid>
</body>
</html>
Localizing the UI
Strings that appear in the UI, either in predefined LIghtroom controls or menus, or in those you define, can
be localized using the LOC function, as described in Chapter 5, “Using ZStrings for Localization.
The LOC function looks up localized values in string dictionaries; your plug-in must supply these as part of
the plug-in folder. To add string dictionaries to your plug-in, create a strings resource folder in your main
plug-in folder, and name the subfolders with the appropriate language codes. For example:
myWebPlugin.lrwebengine/strings/de/TranslatedStrings.txt
myWebPlugin.lrwebengine/strings/fr/TranslatedStrings.txt
...
Localization occurs when the user publishes the gallery. To get different language versions, the user must
run Lightroom in the desired locale, and publish another version of the gallery.
A typical use of dynamic data is to tie two properties together, so that changing one changes the other. For
example, you might want to control the aspect ratio by making photoSizes.mySize.width to be equal to
photoSizes.mySize.height. To do this, you can use a function definition as the value of one of the
properties. For example:
["photoSizes.large.height"] = function() return photoSizes.large.width end,
["photoSizes.large.width"] = 450,
This function simply accesses and returns the value of another property. You can, however, define a
function to perform some transformation of the related value. You can, for instance, add formatting and
logic using Lua's basic math and string manipulation functions. Lightroom also provides a function,
LrColorToWebColor, that converts an LrColor object to a string representation suitable for use in CSS.
Creating a preview
The iconic preview for a web engine is an SWF movie of a gallery that can be shown in the "Preview" panel
of the Web module. It presents a dynamic preview of the gallery using icons, rather than full-size images.
You can use the iconicPreview top-level gallery-info entry to specify how the iconic preview for your
gallery should be implemented. This entry references a simple Flash® movie (which you must implement
and include in the web-engine folder) that renders an "iconic" representation of each web page, in order
to convey the general look of the web gallery in a simple, stylized form.
flashMovie A string value, the relative path from the root web-engine folder to a compiled Flash
movie (an SWF file).
flashvars A function that returns a table of values to be used in the Flash movie. These are the
model properties that you want represented in the iconic preview.
Each entry in this table is available at the _root level of the Flash movie environment.
Numbers and string values are passed through without any conversions. LrColor
objects are converted to a string representation that is easy to parse in ActionScript™.
Your web-engine folder must include a simple Flash movie that renders the "iconic" representation of each
web page. The ActionScript file that defines your movie can access the global variables provided by the
flashvar entry in iconicPreview, at the top level of the _root object.
2. Create an external interface callback called ready, which Lightroom will poll waiting for your preview
to finish drawing:
_root.ready = 'no';
_root.readyFunc = function(str:String) {
return _root.ready;
}
ExternalInterface.addCallback("ready", _root, _root.readyFunc);
Once your ready function returns "yes", Lightroom takes a screenshot of the Flash movie and
terminates its execution (in order to reduce CPU usage)
3. Initialize default values, so that you can preview your movie without running it in Lightroom.
var numCols;
if( _root.numCols != null ) {
numCols =parseInt( _root.numCols );
}
else {
// default value
numCols = 4;
}
Do this for each model property you are using in your preview.
4. Draw the preview. This can be done by rearranging existing objects that you created in Flash, or simply
by using the drawing primitives of the ActionScript programming language. For example:
var cellSize = 10;
for( x = 0; x < numCols; x++ ) {
_root.beginFill( cellColor, 100 );
_root.moveTo(x*cellSize, y* cellSize );
_root.lineTo( (x+1)* cellSize, y* cellSize );
_root.lineTo( (x+1)* cellSize, (y+1)* cellSize );
_root.lineTo( x* cellSize,+ (y+1)* cellSize );
_root.endFill();
}
This draws as many rectangle as are specified in the numCols Flash variable.
CHAPTER 4: Writing a Web-engine Plug-in Web SDK manifest API 112
AddPage Maps one source LuaPage file from the gallery template directly into the
published gallery.
AddResource Maps one resource file or a set of resource files from the gallery template
AddResources directly into the published gallery.
AddPage
Maps one source LuaPage file from the gallery template into the published gallery. The source file is
interpreted by the LuaPage engine, resulting in an HTML file in the published gallery.
Inputs
filename The path to which to write the file in the published gallery.
template The path to the source LuaPages file, relative to the folder containing this manifest.
Example
AddPage {
filename = "content/pages/myWebPage.html",
template = "myWebPage.html",
}
AddResource
Maps one resource file from the gallery template directly into the published gallery. A resource is not
interpreted, but is simply copied directly.
CHAPTER 4: Writing a Web-engine Plug-in Web SDK manifest API 113
Inputs
source The path to the resource file, relative to the gallery template.
destination Optional. The path to the published gallery to which to copy the resource. By default,
the destination path is the same as the source path.
Example
AddResource {
source = "image.png",
destination = "content/resources/image.png",
}
AddResources
Copies a set of resource files from the gallery template directly into the published gallery. A resource is not
interpreted, but is simply copied directly.
Inputs
source The path to the resource folder, relative to the gallery template.
destination Optional. The path to the published gallery to which to copy the resources. By default,
the destination path is the same as the source path.
Example
AddResources {
source = "resources",
destination = "content/resources",
}
Alternative syntax
Instead of passing a table of named arguments, pass a single string to be used as the source:
AddResources "resources"
AddPhotoPages
Uses a LuaPage template to build a separate page for each photo in the current Lightroom selection.
Inputs
When executing the LuaPages for AddPhotoPages, the following variables are defined in the environment
Example
AddPhotoPages {
template = 'detail.html',
variant = '_large',
destination = "content",
}
AddGridPages
Uses a LuaPage template to build a page for each grid of photos in the current Lightroom selection.
Inputs
When executing a LuaPage specified with AddGridPages(), the following variables are defined in the
environment
In addition, if you use AddGridPages() to add any page, all of the LuaPages in your gallery can use these
environment variables:
Example
AddGridPages {
destination='content',
template='grid.html',
rows=model.nonDynamic.numRows,
columns=model.nonDynamic.numCols,
}
AddCustomCSS
Generates a CSS file using the appearance properties defined in your data model.
When you declare your data model in the galleryInfo.lrweb file, this command exports to CSS all
entries that begin with "appearance.".
Inputs
Example
To specify the background color of the body using CSS, you need a declaration like this:
/* Desired CSS output */
body {
background-color: #ff0000,
}
2. Define the required data model entries in the information file (galleryInfo.lrweb):
return {
...
model = {
...
["appearance.body.background-color"] = "#ff0000",
["appearance.body.cssID"] = "body",
...
},
...
}
3. To make this something the user can edit, add a corresponding control to one of the panel
descriptions in the views section of your information file (galleryInfo.lrweb):
return {
...
views = {
...
myViewFactory.label_and_color_row {
bindingValue = "appearance.body.background-color",
title = "Background",
},
...
...
}
IdentityPlate
Exports an identity plate as a PNG file, if the user chooses to use it.
During a Lightroom preview of the web gallery, the PNG file is always generated, to support a live update
of the model-defined property that controls identity-plate use. If the user chooses not to use an identity
plate, however, the PNG is not exported as part of any export, upload, or preview-in-browser operation.
Inputs
destination (string) The path to the published gallery to which to write the image file.
enabledBinding (string) The plug-in-defined entry in the data model that controls whether to export
the identity plate.
Example
The views section of the model binds the property to the identityPlateExport checkbox (defined by
the Lightroom application):
myViewFactory:identity_plate {
value = bind "lightroomApplication.identityPlateExport",
enabled = bind "appearance.logo.display",
},
CHAPTER 4: Writing a Web-engine Plug-in Web SDK manifest API 117
When the user selects or deselects the checkbox, this binding causes the corresponding model property
(logo.display) to be set to true or false, and thus the corresponding CSS property (.logo) to be set to the
correct image (logo.png), or to none. If the user does not choose to export the identity plate, the file
content/logo.png is not generated on upload.
importTags()
Adds custom tagsets to your gallery (see “Web SDK tagsets” on page 120). This is a function which takes
two ordered parameters:
prefix (string) A short prefix used to identify tags belonging to this tagset. For example, "lr".
tagsetPath (string) A path to the tagset definition file.
Example
3. Use that tagset in any LuaPages file, identifying each defined tag with the specified prefix:
<xmpl:fancyQuote>
A wise man once said:<br>
Don’t count your chickens before they’re hatched
</xmpl:fancyQuote>
LuaPage syntax
A LuaPage is a Lua-language source file that is evaluated to produce one destination web page in your
published gallery. In the manifest, use the AddPage command to map each source LuaPage to a
destination file location.
Variable Description
getImage(imageIndex) A function that returns an imageProxy.
mode ➤ When the gallery is being previewed inside Lightroom, the value is the
string "preview".
table A subset of the default Lua table namespace; contains the insert function.
ipairs Standard Lua functions
pairs
type
tostring
LOC Text values can be localized. Use this function as a string value in order to
specify a string by a unique identifier; your plug-in must provide a string
dictionary in which to look up the display-string value for the current system
language. See “Localizing the UI” on page 109."
LrTagFuncs A table of private helper functions for the lr: tags
CHAPTER 4: Writing a Web-engine Plug-in LuaPage syntax 119
Variable Description
includeFile() An execution-time function that allows a page to include another file using
runtime logic to specify which file. For example:
<html>
<body>
<h1>My web gallery</h1>
imageRendition An object that represents an image rendition for a photo. It has these
properties:
width (number) The width in pixels.
height (number) The height in pixels.
relPath (array of string) An array of directory names, in which the last entry is the
file name.
dir (array of string) An array of directory names.
CHAPTER 4: Writing a Web-engine Plug-in Web SDK tagsets 120
At run time, your LuaPage, replaces the tag with its Lua-language tag definition, which it then compiles
and executes to product the HTML output.
There is built-in set of tags included with the Lightroom SDK, which you can also include and use in your
LuaPages.
tags = {
tagName = {
startTag = "macroCode",
endTag = "macroCode",
},
}
The value of the startTag and endTag element is a string containing Lua code. It can use global functions
and constants defined in the same page using a globals table. This is again a table in which each element
defines one function or constant:
globals = {
functionName = function( x )
_body of function_
end,
}
When the LuaPage is evaluated, the code for each tag is evaluated, and the result is substituted for the
opening or closing named tag.
For example, you could define code in this format in your tagset file:
globals = {
myOpenTagFunction = function( )
--body of function
CHAPTER 4: Writing a Web-engine Plug-in Web SDK tagsets 121
end,
myCloseTagFunction = function( )
--body of function
end,
}
tags = {
myTag = {
startTag = "myOpenTagFunction()",
endTag = "myCloseTagFunction()",
}
}
If you import this tag into the xmpl namespace, your LuaPage would reference the tag like this:
<xmpl:myTag>Helloworld</xmpl:myTag>
At run time, when the LuaPage is evaluated, the tags are replaced with the Lua code, and the contents is
simply written out:
myOpenTagFunction() write( [[Helloworld]] ) myCloseTagFunction()
This code is then evaluated to produce the final HTML for your web gallery page.
1. Include the tagset definition file or files in the root directory of your web engine.
This includes all of the tags defined in the file under the namespace lr. The namespace definition
prevents conflicts with tags of the same name defined in other tagset libraries. You can use any
namespace for your own tags.
3. To load the built-in default tagset, substitute the special value "com.adobe.lightroom.default" for the
path:
importTags( "lr", "com.adobe.lightroom.default" )
4. To use the defined tags in your LuaPages, use the namespace prefix for both the opening and closing
tag. For example:
<lr:ThumbnailGrid>...</lr:ThumbnailGrid>
Here is an example that simply wraps some constant text around the text specified as the content of the
tag:
1. Define the tag and its supporting function in the tagset file, myTags.lua:
globals = {
myFn = function( x )
CHAPTER 4: Writing a Web-engine Plug-in Web SDK tagsets 122
2. Use the importTags() command in your web SDK manifest (manifest.lrweb) to import this into the
"xmpl" namespace:
importTags( "xmpl", "myTags.lua" )
4. When the LuaPage file is converted into Lua code, this becomes:
myFn( function() write( [[Helloworld]] ) end )
5. When the Lua code is executed, this produces text as its HTML output:
You said, "Helloworld!"
This tagset is typically imported into the lr: tagset namespace, but you can import it into any namespace
using the importTags() command of your web SDK manifest. As for all imported tags, you must reference
each opening and closing tag name with the namespace prefix. For example:
<lr:ThumbnailGrid>
...
</lr:ThumbnailGrid>
The built-in tagset defines two groups of tags, for building thumbnail grids, and for defining navigation
properties of a multi-page gallery.
Use these tags to build a grid. This set of tags simplifies assembly of repeating units based on rows,
columns, and the photo selection. You can use these tags only on pages that you specify in the manifest
with AddGridPages.
The ThumbnailGrid tag is a container for the other tags, which define cells within the grid. For example:
<lr:ThumbnailGrid>
<lr:GridPhotoCell>
<img src="thumbs/<%= image.exportFilename %>.jpg" >
</lr:GridPhotoCell>
</lr:ThumbnailGrid>
CHAPTER 4: Writing a Web-engine Plug-in Web SDK tagsets 123
This defines a simple grid with only one cell, which displays a photo from the referenced file. It uses a
variable, image, which is evaluated at run time as a reference to the currently selected photo.
The following local variables are available in the context of the ThumbnailGrid tag:
Grid tags
ThumbnailGrid Provides the definition of a thumbnail grid for pages in your gallery. Contains
the remaining tags as children.
GridPhotoCell Defines content to be repeated for each cell. Contained in a ThumbnailGrid
tag; for example:
<lr:ThumbnailGrid>
<lr:GridPhotoCell>
<img src=
"$mypath/thumb/<%= image.exportFilename %>.jpg"
id="<%= image.imageID %>" class="thumb" />
</lr:GridPhotoCell>
</lr:ThumbnailGrid>
Pagination tags
This set of tags can be used to add page navigation buttons to your HTML pages. Predefined
page-navigation buttons include one for the current page, one for direct access to other pages, and ones
for the next and previous page, which can be disabled for the first and last pages. You can associate your
own text or destination with each type of button. For example:
<% if numGridPages > 1 then %>
<div class="pagination">
<ul>
<lr:Pagination>
<lr:CurrentPage>
<li>$page</li>
</lr:CurrentPage>
<lr:OtherPages>
<li><a href="$link">$page</a></li>
CHAPTER 4: Writing a Web-engine Plug-in Web SDK tagsets 124
</lr:OtherPages>
<lr:PreviousEnabled>
<li><a href="$link">Previous</a></li>
</lr:PreviousEnabled>
<lr:PreviousDisabled>
<li>Previous</li>
</lr:PreviousDisabled>
<lr:NextEnabled>
<li><a href="$link">Next</a></li>
</lr:NextEnabled>
<lr:NextDisabled>
<li>Next</li>
</lr:NextDisabled>
</lr:Pagination>
</ul>
</div>
<% end %>
The following local variables are available in the context of the Pagination tag:
Pagination tags
Pagination Provides the definition of pagination properties for pages in your gallery.
Contains the remaining tags as children.
CurrentPage Defines an icon or text for the current page.
OtherPages Defines an icon or value with which to navigate directly to other pages.
PreviousEnabled Defines an icon or value with which to navigate to the previous page.
PreviousDisabled Defines an icon or value for the previous-page button for the first page (the
case in which there is no pervious page).
NextEnabled Defines an icon or value with which to navigate to the next page.
NextDisabled Defines an icon or value for the next-page button for the last page (the case in
which there is no next page).
CHAPTER 4: Writing a Web-engine Plug-in Web HTML Live Update 125
While a page from your gallery is being previewed in Lightroom, a user might change a model variable
using the control panel. In order to reflect the change in the previewed page, the Lightroom browser
normally needs to reload the page. Lightroom clears all cached copies of the page, tells the browser to
reload, and builds new HTML and CSS files in response to the browser’s reload request. This process is time
consuming, and can cause changes in color or other visually startling changes as the page loads. For a
change as simple as a nudge in hue of a color slider, you might find this response unacceptably jarring.
Live Update is intended to avoid browser reload, which disrupts the user experience. Live Update is a
mechanism by which a web engine can intercept and prevent the reload operation, using DHTML/AJAX
scripting techniques to alter the web page in place. DHTML/AJAX use JavaScript, which is executed in the
context of the built-in web browser (rather than the Lua scripting environment of Lightroom in general).
An HTML page in your web engine can incorporate JavaScript that uses Live Update to interact with
Lightroom during a preview. This communication operates in both directions:
➤ Lightroom sends messages to the page, making liveUpdate() JavaScript function calls into the page
whenever the user alters a parameter in the gallery data model. If the call is successful, Lightroom does
not request a page reload.
➤ The page contains JavaScript that sends messages back to Lightroom in response to user events, such
as a request for a text field edit, or to override a data model value.
In order to enable this functionality, your plug-in must contain JavaScript implementations of the
liveUpdate() functions and the event-handler callbacks. There is a sample implementation in the file
live_update.js, which you can use or modify. It is part of the sample plug-ins provided with the SDK.
To include the JavaScript file that implements Live Update in your pages, use a line such as this in your
header.html template file:
➤ The document.liveUpdate function handles changes that involve gallery appearance (such as CSS
properties) and text labels
➤ The document.liveUpdateImageSize function handles changes that involve gallery image size.
Implementation of either of these functions is optional. For simple galleries, the reload solution may be
adequate. If you do not add any Live Update functions to your document object, Lightroom uses the
default reload behavior.
CHAPTER 4: Writing a Web-engine Plug-in Web HTML Live Update 126
When a change affects a page that the browser has previously cached, Lightroom must ensure that the
browser reloads that page, rather than displaying the cached version. Lightroom also maintains a cache,
which may need to be cleared. Your live-update function signals Lightroom about what behavior to use by
returning one of these strings:
➤ invalidateOldHTML: The browser cache is cleared, and all of the HTML pages in Lightroom's page
cache are cleared. The exported JPEGs remain unchanged. The reload is deferred until the user
navigates away from the currently previewed page.
Return this value if the update is successful, and the change affects only the current HTML page
➤ invalidateAllContent: The browser cache is cleared, and all of Lightroom's page caches (both
HTML and resource files) are cleared. The exported JPEGs remain unchanged. The reload is deferred
until the user navigates away from the currently previewed page.
Return this value if the update is successful, and the change affects any referenced file, such as a
JavaScript or CSS file. This is typically the default case.
➤ failed (or any other return, or throwing an exception): Causes immediate reload, and clearing of both
browser and Lightroom page caches. The exported JPEGs remain unchanged.
Return this value if your function is unable to update the page. Lightroom then commands the
embedded browser to reload the original page.
document.liveUpdate
Your HTML gallery can implement this function to respond to a change made in Lightroom to the
appearance (CSS styling), or a Lightroom update to the fixed strings for the gallery. (Do not use it for
changes to strings associated with a particular image, such as the image name or other metadata; a
change of this kind always causes a reload.)
Your function manipulates the web page objects using JavaScript calls; typically, it locates the document
node and alters the page appearance or content to reflect the change made to the data values. The
function should return a result that indicates whether the update was successful.
newValue The new value (such as "ffffff" for the color white).
cssId The corresponding cssId for the node (such as 'body').
property The CSS property that is changing on this node (such as 'margin').
CHAPTER 4: Writing a Web-engine Plug-in Web HTML Live Update 127
document.liveUpdateImageSize
Implement a separate function for live update of image size. This function is called repeatedly while the
mouse is held down on the image size slider. As soon as the mouse is released, a full reload of the page
occurs, flushing all caches and invalidating all JPEGs.
This function must locate the image using document.getElementById(), and set its dimensions using
appropriate DOM methods.
If unable to perform the live update, return "failed". In this case, Lightroom reloads the browser as often
as it can while the mouse is dragged.
The Lightroom SDK includes the source code for the default HTML web engine, which includes an example
implementation of document.liveUpdate() in the file live_update.js. To include this file in your
project, you must construct your data model to match the names used in the JavaScript.
➤ For any gallery text fields, you must place an id attribute on the immediately enclosing element.
➤ The id value must match the dot-separated path to the corresponding model value defined in the
galleryInfo.lrweb file.
For example, in the default HTML gallery, the site title is in an h1 element. In the template source file it looks
like this:
<h1 onclick="clickTarget( this, 'siteTitle.text' ); "
id="metadata.siteTitle.value"
class="textColor">
$model.metadata.siteTitle.value
</h1>
Notice that the id is the same as the path in the model definition in the galleryInfo.lrweb file:
["metadata.siteTitle.value"] = "Site Title",
NOTE: This example implementation issues a reload for compound cssID values, such as
"#myId.myClass". Create unique classes for such cases to avoid the reload.
CHAPTER 4: Writing a Web-engine Plug-in Web HTML Live Update 128
To call from JavaScript into Lightroom, invoke the callCallback() function defined in live_update.js,
using this syntax:
callCallback( "callback_name", param1, param2, ... );
For example, to call the in-place-edit callback defined in the sample implementation, the JavaScript makes
this call:
callCallback( 'inPlaceEdit', target, bounds.x, bounds.y, bounds.width, bounds.height,
font.fontFamily, font.fontSize, imageID )
Lightroom provides these callback functions that can be invoked from JavaScript using callCallback():
inPlaceEdit = function( target, x, y, Edits a text field at given coordinates on the screen.
width, height, fontFamily, fontSize ) See “Specifying in-place edit” on page 128.
updateModel = function( key, value ) Alters the data model for the given dot-separated key
path.
fetchURL = function( url, callbackName ) Downloads the contents of a given URL and returns it
as a string. This is an asynchronous operation.When
the operation is complete, the result string is passed
to the callback.
Your JavaScript code can call inPlaceEdit() directly, using callCallback(). You must provide these
arguments:
target string The dot-path identifier of a metadata property defined in your model,
such as "metadata.siteTitle.value"
x, y number The bounds of the element on the web page, in pixels. These coordinates
width, height are used to position the edit text window that is temporarily
superimposed on the web page.
CHAPTER 4: Writing a Web-engine Plug-in Web HTML Live Update 129
The JavaScript file live_update.js also provides an easier way to implement in-place edit, by using the
clickTarget() function. This function gets the bounds and font information for a particular node in the
page DOM, and uses it to call the inPlaceEdit() function.
You can add in-place editing functionality to any node containing text by adding code like this to your
HTML:
onclick="clickTarget( this, 'target_property' );"
For example:
<p onclick="clickTarget( this, 'metadata.groupDescription.value' );"
id="metadata.groupDescription.value"
class="textColor">
$model.metadata.groupDescription.value </p>
Notice that the target ID sent to clickTarget(), the ID for the node, and the path in the $model variable
all match.
5 Using ZStrings for Localization
ZStrings are an Adobe convention for defining localization strings. You identify a string according to its
usage in the user interface, and specify it in the ZString format. This enables Lightroom to look up
language-specific versions of the string to display to the user.
➤ In Lightroom, you pass ZStrings to the built-in LOC function to allow for localization of your plug-in’s
displayed text. See “The LOC function” on page 132.
➤ Resolution of ZStrings depends on dictionary files that you supply, which contain the mappings from
the ZString path to the localized string. See “Localization dictionary files” on page 133.
NOTE: Reloading a plug-in interactively or automatically after export does not reload any localization
dictionaries supplied with that plug-in. The translation dictionaries are read only when the plug-in is
first loaded or Lightroom is restarted.
ZString format
The format of a ZString is:
$$$/ZString_path/stringKey=defaultValue
$$$ The ZString marker is always required to identify a ZString and distinguish it from
any other 8-bit ASCII string.
/ZString_path/ The path and key uniquely identifies a specific string, and is used to look up the
stringKey= translation in a dictionary file that you provide with your plug-in (see “Localization
dictionary files” on page 133.)
The path is a series of 7-bit ASCII character strings separated by the slash (/)
character. You can use any strings you wish, except that no white space is allowed.
The last element of the path is a specific key name, which is separated from the
default value by an equal sign (=).
The path groups a set of properties; for example, you might use a unique path for a
particular plug-in, and within that plug-in further group all strings that appear in a
particular dialog.
Each plug-in has its own mapping of the context paths, so your path names will not
conflict with those used by other plug-ins, or by Lightroom itself.
defaultValue The string following the separator (=) is the default display string to use for this
ZString. If no matching key exists in the active localization dictionary (or if no
appropriate dictionary is found) this value is displayed to the user.
Strings values used in ZStrings can contain escape sequences to indicate certain
characters; see “ZString characters and escape sequences” on page 131.
130
CHAPTER 5: Using ZStrings for Localization ZString format 131
Like any Lua string, ZStrings can be enclosed in single or double quotes. For example:
LOC "$$$/MyPlugin/Dialogs/Description/sectionName=Description"
LOC '$$$/MyPlugin/Dialogs/Description/Title=Document Title:'
➤ ZStrings allow some common non-low-ASCII characters to be substituted for escape sequences in the
strings. For example, the sequence ^T includes the trademark symbol (™) in that location in the
resulting string.
➤ The general-purpose sequence ^U encodes any arbitrary Unicode character by its code point, in the
form ^U+1234.
➤ An escape sequence with a number is a replacement point.; the sequence to be replaced by another
string supplied as an additional argument to LOC(); see “The LOC function” on page 132.
Sequence Replacement
^r carriage return
^n line feed
^B bullet
^C copyright
^D degree
^I increment
^R registered trademark
^S n-ary summation
^T trademark
^! not
^{ left single straight quote
^} right single straight quote
^[ left double quote
^] right double quote
^' right single curly quote
^. ellipsis ("...")
^e Latin small e with acute accent
^E Latin small e with circumflex
CHAPTER 5: Using ZStrings for Localization The LOC function 132
Sequence Replacement
^d Greek capital delta
^L backslash ("\")
^V vertical bar ("|")
^# command key (in Mac OS)
^` accent grave ("`")
^^ circumflex ("^")
^0 - ^9 Marks insertion point for additional LOC argument strings; see “The LOC
function” on page 132.
^U+xxxx Unicode code point U+xxxx
You can use the LOC function anywhere you specify display strings:
Any of these properties can take a simple string or a LOC and ZString value. You are not required to use the
LOC function if you do not need to localize the text of your plug-in.
Here is an example of localizing the text that identifies an Export Service Provider in the Export destination
section of the Export dialog:
LrExportServiceProvider = {
title = LOC "$$$/MyPlugin/Nmae=My Plug-in",
file = 'MyPluginExportServiceProvider.lua',
...
},
The LOC function also allows you to combine strings using placeholders in the ZString’s value string, and
additional string arguments to the function. The placeholders use “hat” notation with a numeric value; the
first is ^1, the second ^2, and so on. You can specify up to 9 additional string arguments, which are
inserted at the placeholder locations ^1 through ^9 in the localized text.
For example:
LOC( "$$$/Message=Could not open the file ^1 because ^2.",
"myfile.jpg", "a disk error occurred" )
The placeholders are replaced by the string arguments, resulting in this string:
"Could not open the file myfile.jpg because a disk error occurred."
CHAPTER 5: Using ZStrings for Localization Localization dictionary files 133
➤ Each translation dictionary must be in a file named TranslatedString_code.txt. The code is the
two-letter ISO code for the language, such as en for English, fr for French, de for German, ja for
Japanese, and so on.
➤ The dictionary files must located be in the top plug-in folder, with the Info.lua file.
Lightroom automatically selects the appropriate translation file based on the current language in use for
the application.
Lightroom performs ZString translation when it creates the object containing the string. When the LOC
function encounters a ZString, it looks for the localization dictionary appropriate to the current locale, and
uses it to find translations for static ZString values.
➤ If there are no localization dictionary files, or if none is found to match the application language, the
LOC function returns the value string found in the original ZString.
➤ When it does find a matching dictionary file, the LOC function locates the ZString in the dictionary file
using the context path and property name; that is, the first part of the supplied ZString up to, but not
including the = sign. If a matching line is found, LOC removes the first part of the found ZString up to
and including the = sign, and returns the remaining string as the translation.
➤ If a matching line is not found in the dictionary, the function returns the value string found in the
original ZString.
The only things allowed on a line (after the first character) are ZStrings; no newline characters or
comments are allowed. The ZStrings in this file must all be enclosed with double quotes.
The following text editor are recommended for creating these files:
➤ In Mac OS X simply use TextEdit and make sure you save the file type as “UTF-8” (rather than UTF-16 or
UCS-2, for instance).
➤ In Windows use Notepad, and be sure to save the file as type “UTF-8.”
The file is UTF-8 formatted text. A leading UTF-8 byte-order marker (EF BB BF) is permitted.
CHAPTER 5: Using ZStrings for Localization Localization dictionary files 134
The Lightroom SDK includes some complete sample plug-ins that you can examine and use to familiarize
yourself with the plug-in architecture, and with API and Lua usage in the Lightroom SDK.
The plug-in samples are packaged with the Lightroom SDK, in the folder LR_SDK/Sample Plugins/ (see
“The Lightroom SDK” on page 7).
The plug-in script files are written using the Lua scripting language which have the file extension .lua.
Each section in this chapter lists the program files and support files that are provided in the plug-in folder
for each sample.
➤ “The FTP Upload sample plug-in” on page 136 demonstrates how to use the SDK API to connect to an
FTP server and upload images using FTP.
➤ “The Flickr sample plug-in” on page 140 demonstrates how to use the SDK API to upload images
directly to a Flickr account using HTTP.
Each of these samples is an Export Service Provider, extending Lightroom's Export dialog by adding a
new export destination. The plug-ins define their own export settings, as needed for their operations,
and add one or more sections to the Export dialog that allow the user to make settings choices for the
export operation.
In addition, the samples demonstrate how to define and use independent dialogs for confirmations
and actions.
➤ “Metadata and filtering samples” on page 145 demonstrate additional types of standard plug-in
functionality. These show how to create Lightroom-specific metadata and use it together with other
features, such as customizing the Plug-in Manager, creating dialog boxes, and creating an Export Filter
Provider that accesses custom metadata.
➤ “Post-processing samples” on page 149 demonstrate more types of post-processing that can be
accomplished with an Export Filter Provider.
➤ “Web engine sample” on page 151 demonstrates a different type of plug-in, a web engine, by creating
a simple HTML gallery.
135
CHAPTER 6: SDK Sample Plug-ins The FTP Upload sample plug-in 136
The following steps show how to use the FTP plug-in and guide you through exporting images to an FTP
server.
2. In the Lightroom Library module, make sure you have at least one image available for export, then
choose File > Export to bring up the Export dialog.
3. Use the Export destination list at the top of the Export dialog to select the FTP Upload plug-in:
This loads the FTP plug-in and displays the additional FTP Server section it defines for the Export
dialog.
CHAPTER 6: SDK Sample Plug-ins The FTP Upload sample plug-in 137
This displays the configuration dialog for the FTP server settings.
CHAPTER 6: SDK Sample Plug-ins The FTP Upload sample plug-in 138
➣ Server: Enter the name of the FTP server you wish to connect to, for example:
myftpserver.adobe.com. You do not need to enter the protocol.
➣ Username: Enter the username you use to log into your FTP Server.
➣ Password: Enter the password you use to log into your FTP Server. If you wish, check the ‘Store
password in preset’ checkbox.
➣ Protocol: Select the protocol from the drop down menu. The default is FTP.
➣ Port: If your FTP servers uses a port other than port 21, enter the number.
➣ Server Path: If you need to add the path to your home folder on the FTP server, you can enter the
path, or you can click Browse to browse the remote file system.
Navigate to your desired folder and click Select. This returns you to the FTP Configure File Transfer
dialog.
CHAPTER 6: SDK Sample Plug-ins The FTP Upload sample plug-in 139
3. To store this configuration in a preset, bring up the Preset popup and select Save Current Settings as
new Preset.
In the resulting dialog, enter a name for your preset and click Create. Lightroom connects to your FTP
server and displays a Browse dialog.
5. If you want to upload your images to a subfolder within your home folder, select the Put in Subfolder
checkbox in the FTP Server section of the Export dialog.
This enables the text field, where you can enter the folder name. The default subfolder name is
‘photos’. You can enter another single folder name, or create a subfolder hierarchy be entering the
path; for example myphotos/myotherfolder/.
The bottom the FTP Server section of the Export dialog displays the full path to which your images will
be uploaded at.
2. A progress indicator appears in the upper-left corner of the Lightroom catalog window, which allows
you to monitor the progress of the Export operation.
CHAPTER 6: SDK Sample Plug-ins The Flickr sample plug-in 140
➤ Localizing strings
presets/Flickr – Public.lrtemplate Predefined template for the plug-in with public settings.
Frob http://www.flickr.com/services/api/flickr.auth.getFrob.html
Auth Tokens http://www.flickr.com/services/api/flickr.auth.getToken.html
1. Use the Plug-in Manager to add the plug-in, found in the Lightroom SDK samples folder:
LR_SDK/Sample Plugins/flickr.lrdevplugin.
2. In Lightroom Library module, make sure you have at least one image available for export, then choose
File > Export to bring up the Export dialog.
3. Use the Export destination list at the top of the Export dialog to select the Flickr plug-in:
The plug-in’s Export Service Provider defines text for the top section that shows when the service is
selected, and adds new sections to the Export dialog. Notice that is also adds a status message at the
bottom, next to the Plug-in Manager button.
Log in to Flickr
1. In the Flickr Account section of the Export dialog, click Log In.
This invokes a dialog in which to enter the API Key and Shared Secret that you need to log in to your
Flickr account:
This dialog and its contents are defined by the plug-in code in EnterApiKey.lua.
2. Enter your API Key and Shared Secret and click OK.
If you don’t have an API Key and Shared Secret, click Get Flickr API Key. Your default web browser
opens and displays a web page instructing you how to obtain your own API Key and Shared Secret.
Follow the on-screen instructions, then enter the information in the dialog and continue.
3. The next dialog informs you that you must authorize Lightroom in order for the plug-in to correctly
upload images. This dialog is defined by the plug-in code in FlickrUser.lua.
Click Authorize.
CHAPTER 6: SDK Sample Plug-ins The Flickr sample plug-in 143
4. The plug-in displays a web page where you must log into your Flickr account. Log in with the correct
Flickr username and password for the account.
When you have logged into Flickr, it shows a web page stating that the application is authorized.
5. Return to Lightroom and click Done. This returns you to the Export dialog
➣ Once you have logged in, notice that the Log In button in the Flickr Account section has changed
to Switch User; see “Changing the Flickr account” on page 143.
➣ Notice also that the Export button at the bottom is now enabled.
1. In the Export dialog, click Export to initiate the plug-in’s upload operation.
2. You can monitor the progress of the Export operation in the progress indicator that appears in the
upper-left corner of the Lightroom catalog window.
3. When the upload operation is finished, your images have been exported to Flickr. The plug-in opens a
Flickr web page in your browser, listing your images and notifying you that the upload was successful.
Once you have logged in to one Flickr account, you can use the Switch User button to change the account
to which your images are uploaded.
1. Click Switch User. This invokes the authorization dialog, shown in step 3 on page 142.
3. Log in to the new Flickr account with the correct username and password for that account.
Plug-in settings
The plug-in defines settings that allow you to alter how your images are uploaded. For example, you can
adjust the privacy settings for the images you upload, and specify additional tags for the uploaded images.
The plug-in supplies two user presets, which are collections of settings with particular values. These are
stored in a subfolder named presets, within the plug-in folder.
Lightroom preset files are identified by the .lrtemplate extension. A preset file defines a table with all of
the necessary fields to enable the plug-in to work correctly, plus additional Lightroom-defined settings for
such things as the JPEG quality and size constraints.
One of the plug-in-defined export setting is for the privacy status of uploaded photos. By default, this is set
to Private. You can modify it by adjusting the settings in the Privacy and Safety section that the plug-in’s
Export Service Provider added to bottom of the Export dialog.
The plug-in also adds the Organize section, where you can add extra keyword tags to the images to be
uploaded.
CHAPTER 6: SDK Sample Plug-ins Metadata and filtering samples 145
2. In the Metadata panel of the Library module, open the menu at the top left and choose Custom
Metadata.
3. The custom metadata created by the plug-in appears in the metadata panel.
CHAPTER 6: SDK Sample Plug-ins Metadata and filtering samples 146
4. Select a photo.
5. Change the Display Image value to Yes, and the Random String value to Test.
2. Select several photos in the Library module, including the one that you modified in the step 5 above.
3. From the Library menu, choose Plug-in Extras > Custom Metadata Dialog (an item added by this
plug-in).
CHAPTER 6: SDK Sample Plug-ins Metadata and filtering samples 147
4. A dialog defined by this plug-in appears, showing the name of the photo for which you set the Display
Image value to Yes:
2. Select some photos in the Library module, noting the Title metadata values for one or more.
4. Notice the entry for this plug-in, Metadata Post Process, in the Post-Process Actions panel.
5. Open the entry by clicking the gray arrow at the left, select the action, click Insert.
6. Notice the check mark by the action, which indicates that it has been inserted into the export
operation, and the related dialog section at the right, which has been defined by this plug-in.
CHAPTER 6: SDK Sample Plug-ins Post-processing samples 149
7. In the new Metadata Export Filter dialog section, choose Title from the drop-down menu, and enter
the title of one of your selected photos in the edit text field.
Only the single picture whose Title value matches the one you entered is exported; all the other
photos in your selection are removed from the export operation by this Export Filter Provider.
Post-processing samples
The samples creatorfilter.lrdevplugin and languagefilter.lrdevplugin provide additional
examples of post processing, using Export Filter Providers. These plug-ins show the typical construction of
Export Filter Providers, making use of an external application to process XMP metadata. Together, the
samples demonstrate how you can combine multiple post-processing actions, allowing the user to choose
one, both, or neither of the actions.
The Creator External Tool (defined in creatorfilter.lrdevplugin) also includes the metadata filter
logic defined in the “Metadata filter sample” on page 147, which excludes files with matching metadata
from the export operation. This illustrates how to combine a simple exclusion filter with the external
post-processing that writes XMP metadata.
Plug-in
files creatorfilter.lrdevplugin Allows the user to add or modify certain XMP
metadata values to photos being exported.
Info.lua The Export Filter Provider information
CreatorExternalToolFilterProvider.lua and definition script.
win\LightroomCreatorXMP.exe
mac/LightroomCreatorXMP
The platform-specific external XMP
application that performs the selected
action.
languagefilter.lrdevplugin Allows the user to update one of the
localized values for the Title property in the
XMP metadata.
Notice the entry for these plug-ins, Creator External Tool and Language External Tool, in the
Post-Process Actions panel. Each plug-in defines one action.
4. Open the Creator External Tool entry by clicking the gray arrow at the left, select the action, and click
Insert (or simply double-click the action).
The section defined for that action appears, allowing the user to enter a value for the creator-name
XMP property.
CHAPTER 6: SDK Sample Plug-ins Web engine sample 151
Notice that this action also includes the metadata filter—that is, it allows the user to exclude photos
whose plug-in-defined metadata values match the users choice. The logic for this part is the same as
that defined in the “Metadata filter sample” on page 147.
5. Select and insert the action under Language External Tool; the section for that action is added,
allowing the user to select a language and new value for that language’s translation value of the XMP
Title property.
6. Remove one or both of the actions from the processing queue, and observe the changes in the dialog.
You can remove an action by double-clicking the name in the Post-Process Actions section, by
selecting the action and clicking Remove, or by using the X icon in the upper right corner of the
corresponding dialog section.
7. Try changing the order of the actions using the up and down arrows in the upper right corner of the
corresponding dialog section.
8. With one or both of the actions inserted in the queue, make choices in the dialog sections, and click
Export to begin the export operation.
9. Open the exported photos in any tool that shows XMP metadata and observe the result.
➣ Any photos you filtered out based on Lightroom metadata values should not have been exported.
➣ The XMP metadata should reflect the value you entered for Creator, and the translation you
entered for Title.
Plug-in
files manifest.lrweb The manifest maps LuaPage source files and template
files to Web Gallery HTML output files using a set of
commands for different kinds of pages and resource
files.
galleryInfo.lrweb Defines the data model and UI for the gallery.
grid.html Template LuaPages, HTML with embedded Lua and
header.html JavaScript code.
footer.html
3. Select the web engine defined by this plug-in that appears in the Engine list:
4. The gallery preview appears, showing a filmstrip of small images on one side and a larger version of
the selected image on the right.
7 Getting Started: A Tutorial Example
This chapter will help you get started with extending Lightroom’s Export behavior by walking through the
creation of the simple Hello World plug-in. This plug-in adds menu items to the File and Library menus,
and defines dialog boxes that are displayed when the menu items are selected. The plug-in also
demonstrates how to output and view trace information for debugging and development.
This chapter shows how to build plug-ins that extend the Export functionality of Lightroom. The concepts
and techniques are explained in more detail in Chapter 2, “Writing a Lightroom Plug-in.”
➤ Additional features you can add using the same framework are demonstrated in Chapter 8, “Defining
Metadata: A Walkthrough.”
➤ Web Gallery plug-ins, which use a different framework, are demonstrated in Chapter 9, “Web Gallery
Plug-ins: A Tutorial Example.”
You must describe your plug-in to Lightroom by creating an Info.lua file and placing it in your
plug-in folder. This script must return a table that describes the plug-in to Lightroom.
2. Edit the script in the information file to return a table. This table must contain the version number for
the SDK and a unique string to identify the plug-in.
3. Add another entry to the returned table to create a menu item in the Lightroom File menu.
4. Add another entry to the returned table to create a menu item in the Lightroom Library menu.
153
CHAPTER 7: Getting Started: A Tutorial Example Displaying a dialog 154
➤ The item that we have added to the File menu, Hello World Dialog, appears under the Export section
of that menu. It displays one of the SDK’s predefined dialog boxes.
➤ The item that we have added to the Library menu, Hello World Custom Dialog, displays a customized
dialog box.
The Lightroom SDK provides the facility to display both predefined and customized dialogs using the
LrDialogs namespace. To give your script access to a namespace you must import the namespace with
the import() function. You can then use the namespace functions to specify and invoke the dialogs.
Now we will walk through creating the service scripts for the two menu items.
1. Create the files ExportMenuItem.lua and LibraryMenuItem.lua, and save them in the plug-in
folder.
Displaying a dialog
This example demonstrates a simple service script that displays one of the predefined dialogs. It shows
how to import the LrDialogs namespace, and create a function to display the message dialog, with a
script-defined message.
3. In the body of your function, use the LrDialogs namespace function message() to present a
predefined modal message-display dialog, which displays the simple text ‘Hello World.’
CHAPTER 7: Getting Started: A Tutorial Example Displaying a custom dialog 155
4. To call the function when the script runs, place this line at the end of the script:
MyHWExportItem.showModalDialog()
We will check the result after we have set up the second menu item.
1. Edit the LibraryMenuItem.lua file to import the following namespaces and classes:
local LrFunctionContext = import 'LrFunctionContext'
local LrBinding = import 'LrBinding'
local LrDialogs = import 'LrDialogs'
local LrView = import 'LrView'
local LrColor = import 'LrColor
3. To get the function context, add the following code inside the showCustomDialog() function:
-- body of show-dialog function
LrFunctionContext.callWithContext( "showCustomDialog", function( context )
-- body of called function
end)
Notice that the second argument is the main function, which is passed an LrFunctionContext object.
4. In the body of the main function, create an observable table using the LrFunctionContext object.
Create UI elements
The Lightroom SDK also provides the LrView class and namespace which allows you to create custom
dialog elements. You need to populate the custom dialog with a view hierarchy that defines the custom-UI
portion of the dialog.
We imported the LrView namespace with the import() function. Now we will use the namespace
function LrView.osFactory() to obtain a view-factory object, then use that object to create the UI
elements.
7. The variable c will hold the view hierarchy that defines the dialog contents. The root node is a row
container, and it is bound to the observable data table that we created in step 4 above. All of the child
nodes inherit this binding, so that they can easily reflect and set data values in this table.
8. Add a checkbox control as a child of the row, and bind it to the isChecked property we created in step
5:
-- add controls
f:checkbox {
title = "Enable", -- label text
value = LrView.bind( "isChecked" ) -- bind button state to data key
},
9. Create an editable text field, setting the value to some arbitrary text. This field will only be enabled
when the checkbox is checked:
f:edit_field {
value = “Some Text”,
enabled = LrView.bind( "isChecked" ) -- bind state to same key
},
}
CHAPTER 7: Getting Started: A Tutorial Example Displaying a custom dialog 157
10. Use LrDialogs.presentModalDialog() to display the custom dialog. The argument is a table with
entries for the dialog title and the view hierarchy that defines the contents:
local result = LrDialogs.presentModalDialog(
{
title = "Custom Dialog",
contents = c, -- the view hierarchy we defined
}
)
11. To call the function when the script runs, add this at the bottom of the script:
MyHWLibraryItem.showCustomDialog()
1. In Lightroom, choose File > Plug-in Manager to show the Plug-in Manager dialog.
➣ If you have not yet added this plug-in to Lightroom, click Add, navigate to the plug-in folder you
created, and click Add Plug-in.
➣ If you added the plug-in earlier, reload it. Open the Plug-in Author Tools section and click Reload
Plug-in.
2. Choose File > Plug-in Extras > Hello World Dialog to show the predefined modal dialog created by
the ExportMenuItem.lua script.
4. Choose Library > Plug-in Extras > Hello World Custom Dialog to show the custom modal dialog
created by the LibraryMenuItem.lua script:
5. This example isn’t very interesting yet, since no other controls are bound to data values. Click OK or
Cancel to dismiss, the dialog, and we will add some more complex bindings and behavior.
CHAPTER 7: Getting Started: A Tutorial Example Transforming data 158
Transforming data
The very simple binding we created for the checkbox allows you to set and clear a data value by selected
or deselecting the checkbox button. To show a more complex relationship between the UI and the data,
we will add two radio buttons and a static text field. All three are bound to the same data key, but with
transformations such that when you select one radio button, it deselects the other, and updates the text to
show which is selected.
2. Within this function, make the function-context call you need for the property table:
LrFunctionContext.callWithContext( "showCustomDialogWithTransform",
function( context )
-- body of function
end )
3. In this context, create the observable table, and add a property named selectedButton, with an
initial value:
-- body of function
local props = LrBinding.makePropertyTable( context )
props.selectedButton = "one" -- new property with initial value
-- create view hierarchy
4. Now we will create a new view hierarchy for the dialog, whose controls are bound to this table. This is
a slightly more complex hierarchy, where the root node is a column container, which has two rows.
The rows contain the controls, two radio buttons and a text box:
-- create view hierarchy
local f = LrView.osFactory() -- get the view factory object
local c = f:column {
bind_to_object = props, -- all controls bound to our table
spacing = f:control_spacing(), -- default spacing for the child rows
f:radio_button {
title = "Button two",
checked_value = "two",
-- add value binding in next step
},
},
},
5. For both buttons, add the following to bind the current value of both to the same key:
-- add value binding in next step
value = LrView.bind( "selectedButton" ),
Now this key will reflect the user’s choice of buttons; selecting a button will set the key value to "one"
or "two".
6. Add the title for the static text box. Instead of binding it directly to the key value, we will transform
that value into a display string. To do this, we make the argument of the bind() function a table,
containing the key and a transform function:
-- add title with binding later
title = LrView.bind
{
key = "selectedButton",
transform = function( value, fromTable )
-- body of function
end,
}
8. Use LrDialogs.presentModalDialog() to display the new custom dialog. The argument is a table
with entries for the dialog title and the view hierarchy that defines the contents:
local result = LrDialogs.presentModalDialog
{
title = "Custom Dialog Transform",
contents = c, -- the view hierarchy we defined
}
9. To call the function when the script runs, add this at the bottom of the script:
MyHWLibraryItem.showCustomDialogWithTransform()
2. Choose Library > Plug-in Extras > Hello World Custom Dialog to show the custom modal dialog
created by the LibraryMenuItem.lua script:
3. Select the different buttons and notice how the text changes dynamically, reflecting your selection.
➤ This dialog will show how to update a numeric data value using a slider. It will update two data values,
in two different tables, with two sliders.
➤ We will then bind a text field to the two keys, transforming the numeric values to text.
➤ Because the keys are in different tables, we will need to override the default table for the control by
providing the table specification with the key specification.
This example also demonstrates a slightly more complex containment hierarchy, with some layout and
appearance features.
function MyHWLibraryItem.showCustomDialogWithMultipleBind()
-- body of show-dialog function
end
CHAPTER 7: Getting Started: A Tutorial Example Binding to multiple keys 161
2. In the body of this function, add code to create the function-context call you need for the property
table:
-- body of show-dialog function
LrFunctionContext.callWithContext( "showCustomDialogWithMultipleBind",
function( context )
-- body of called function
end )
4. Create a data key for each of the sliders, one in each table, with an initial numeric value:
tableOne.sliderOne = 0
tableTwo.sliderTwo = 50
5. At the top level, create the view hierarchy for the dialog. In this one, the root node is a column
container with one row, and the controls in the row are grouped together using a group box
container:
local f = LrView.osFactory() -- obtain the view factory object
f:row {
f:group_box {
title = "Slider One",
font = "<system>",
f:slider {
value = LrView.bind( "sliderOne" ), -- simple binding in default table
min = 0,
max = 100,
width = LrView.share( "slider_width" ) -- shares width of other slider
},
f:edit_field {
place_horizontal = 0.5,
value = LrView.bind( "sliderOne" ), -- bound to same key as slider
width_in_digits = 7
},
},
},
f:group_box {
title = "Slider Two", -- no bindings yet, will add those
font = "<system>",
f:slider {
-- add value binding later
min = 0,
max = 100,
width = LrView.share( "slider_width" ) -- shared width
},
CHAPTER 7: Getting Started: A Tutorial Example Binding to multiple keys 162
f:edit_field {
-- add value binding later
place_horizontal = 0.5,
width_in_digits = 7
}
},
f:group_box {
fill_horizontal = 1,
title = "Both Values",
font = "<system>", -- set a font value
f:edit_field {
-- add multi-key value binding later
place_horizontal = 0.5,
width_in_digits = 7,
},
},
}
6. For the value binding in the second slider, we will specify a different bound table, which overrides the
default bound table for that control:
f:slider {
bind_to_object = tableTwo,
value = LrView.bind( "sliderTwo" ),
The two sliders are now bound to different keys in different tables; the user can change the numeric
values using the sliders, and you can see the result in the associated text field for each one.
You will now add code to bind a third text box to a value derived from these two values.
8. To bind a value to multiple keys in different tables, you need to supply both the key name and the
table in the binding, since the control can have only one default bound table.
Add this code to bind the value of the third edit box:
f:edit_field {
-- add multi-key value binding later
value = LrView.bind {
keys = { -- specify the two bound keys
{
key = "sliderOne" -- in default table
},
{
key = "sliderTwo",
bind_to_object = tableTwo - specify a different table
}
},
-- add operation
}),
CHAPTER 7: Getting Started: A Tutorial Example Binding to multiple keys 163
9. You must also supply the function that operates on the multiple key values to supply a single result for
the binding. In this case, we will simply add the two numeric values, and return the result:
-- add operation
operation = function( binding, values, fromTable )
return values.sliderTwo + values.sliderOne
end
},
Notice how you use the values argument passed to this function to access the value of each bound
key. Whenever one of the key values changes, this function is automatically invoked; the return value
becomes the result of the binding, and thus the value of the edit box.
10. Use LrDialogs.presentModalDialog() to display the new custom dialog, and call it when the script
is run:
local result = LrDialogs.presentModalDialog {
title = "Custom Dialog Multiple Bind",
contents = c, -- the view hierarchy we defined
}
MyHWLibraryItem.showCustomDialogWithMultipleBind()
2. Choose Library > Plug-in Extras > Hello World Custom Dialog to show the custom modal dialog
created by the LibraryMenuItem.lua script:
3. Move the sliders and notice how the text below them changes, reflecting the current value for each
numeric property, and how the sum of the two values is displayed in the "Both Values" box.
This example demonstrates how to set up an observer that is notified when a data value changes, and how
to define a function that responds to that notification by setting UI values.
2. Within this function, make the function-context call you need for the property table:
LrFunctionContext.callWithContext( "showCustomDialogWithObserver",
function( context )
-- body of function
end )
3. In this context, create the observable table, and add a property named myObservedString, with an
initial value:
-- body of function
local props = LrBinding.makePropertyTable( context )
props.myObservedString = "This is a string" -- new prop with initial value
4. Obtain a view factory and use it to create a static text field, which initially displays the static value of
the property. (We will put it into the view hierarchy later.)
local f = LrView.osFactory() -- obtain the view factory object
The title, which is the displayed text, is assigned to be the current value of the property we defined
in the data table, props.myObservedString. This is not a dynamic binding, just an assignment to the
current value. So far, if the property value changes, it will not change the text in the control.
5. Create an edit box (which we will also add to the view hierarchy later). Notice that this box updates its
value with every keystroke:
6. When the observer receives a notification it invokes a function. Create the function that will be used
by the observer:
local myCalledFunction = function()
showValue_st.title = updateField.value -- reflect the value entered in edit box
showValue_st.text_color = LrColor( 1, 0, 0 ) -- make the text red
end
This makes the showValue_st text dynamic, by resetting its title value when the observed property
changes. It also turns the text red to show that it has fired.
7. Now add the observer to the observable table. This associates the function with a specific property in
the table:
props:addObserver( "myObservedString", myCalledFunction )
This observer is notified, and calls the response function, whenever the value of the key
myObservedString is modified.
9. We will add one more element, a push button. This demonstrates another way to define the behavior
of your UI, by specifying a direct action to be taken in response to clicking the button. In this case, the
CHAPTER 7: Getting Started: A Tutorial Example Adding a data observer 166
button action resets the observed property value to the value entered by the user in the edit box. It
also resets the color of the static text to black, so that we will be able to tell whether the observer
function fired.
10. Use LrDialogs.presentModalDialog() to display the new custom dialog, and call it when the script
is run:
local result = LrDialogs.presentModalDialog {
title = "Custom Dialog",
contents = c, -- the view hierarchy we defined
}
MyHWLibraryItem.showCustomDialogWithObserver()
2. Choose Library > Plug-in Extras > Hello World Dialog to show the predefined modal dialog created
by the ExportMenuItem.lua script.
4. Choose Library > Plug-in Extras > Hello World Custom Dialog to show the custom modal dialog
created by the LibraryMenuItem.lua script:
6. Click Update. Notice the “Bound value” text changes to whatever text you entered, and the text turns
red.
CHAPTER 7: Getting Started: A Tutorial Example Debugging your plug-in 167
7. Click Update again, without changing the text in the “New value” field. Notice how the text turns
black. This is because the observer is only notified when the bound value changes.
The SDK does not provide a facility to view the debugging output directly; you can write out a log file to
disk, or use a third-party application, such as one of these tools:
➤ Xcode
Specifying a log
Use these steps to add trace information to the Hello World plug-in:
2. After the import statements, create a new logger instance named libraryLogger and enable the
print or logfile action:
➣ Choose print if using a console log viewing tool; see “Viewing trace information in a platform
console” on page 168.
➣ Choose logfile if using a text file for debugging; see “Viewing trace information using log files”
on page 168
myLogger:trace( message )
end
4. Add trace information to the myCalledFunction function. Add the following code:
MyHWLibraryItem.outputToLog( "props.myObservedString has been updated." )
5. Within the action function for the Update button, add the following trace information:
MyHWLibraryItem.outputToLog( "Update button clicked." )
CHAPTER 7: Getting Started: A Tutorial Example Debugging your plug-in 168
1. Start Lightroom.
2. Make sure your plug-in is configured to write debugging information to a log file, as described above:
local LrLogger = import 'LrLogger'
local myLogger = LrLogger( 'libraryLogger' ) -- the log file name
myLogger:enable( "logfile" )
function MyHWLibraryItem.outputToLog( message )
myLogger:trace( message )
end
3. Once your plug-in has generated output, look for a the output file with the name you specified and
the .txt extension ("libraryLogger.txt" in this example).
➣ In Mac OS, the file is located in the Documents folder inside your home directory.
More advanced text editors will automatically notice and update their display when the file has changed;
you may want to use such a text editor.
In Mac OS, you may find it simpler to open a Terminal window and use the tail command to watch the
file by typing a command such as:
tail -f ~/Documents/libraryLogger.txt
1. Start Lightroom.
2. Start WinDbg.
4. In the Attach to Process dialog, scroll through the processes and look for lightroom.exe.
7. In Lightroom, run the Hello World plug-in (see “Run the plug-in” on page 166).
8. In WinDbg, view the console to see the trace information being written as you use the plug-in.
1. Start Lightroom
2. Start Console. The default location is Applications > Utilities > Console.
3. In Lightroom, run the Hello World plug-in (see “Run the plug-in” on page 166).
4. View the console to see the trace information being written as you use the plug-in.
5. You can type the word "Lightroom" into the Filter box in the upper right corner of the Console
window, to suppress log messages from other applications.
8 Defining Metadata: A Walkthrough
This chapter shows how a plug-in can define metadata fields that Lightroom can display along with
standard metadata for photos, and which you can use as a private data model for plug-in processing. It
also illustrates how the private data can be used to customize the Plug-in Manager.
These concepts and techniques are introduced and explained in more detail in Chapter 2, “Writing a
Lightroom Plug-in.”
First, we will create the framework for the plug-in, which is similar to that for an export plug-in.
LrToolkitIdentifier = 'sample.metadata.mymetadatasample',
LrPluginName = LOC "$$$/MyMetadataSample/PluginName=My Metadata Sample",
LrMetadataProvider = 'MyMetadataDefinitionFile.lua',
LrMetadataTagsetFactory = 'MyMetadataTagset.lua',
}
4. Open the file MyMetadataDefinitionFile.lua and add the following code as the initial framework:
return {
metadataFieldsForPhotos = {
},
schemaVersion = 1,
}
The schema-version value provides version control; it can be incremented to notify users of changes to
the plug-in.
171
CHAPTER 8: Defining Metadata: A Walkthrough Adding custom metadata 172
5. The metadataFieldsForPhotos table is where we define our new custom metadata fields.
This is the simplest type of field. It does not have any of the properties that make it visible in the
Metadata panel, or modifiable by users. It is an internal field that a plug-in can use as private data.
Other plug-ins can also access such a field, but they cannot write to it.
7. To make this new field available to edit within the Lightroom Metadata panel, we need to add a title
and data type:
metadataFieldsForPhotos = {
{
id = 'siteId',
},
{
id = 'myString',
title = LOC "$$$/MyMetadataSample/Fields/MyString=My String",
dataType = 'string',
},
},
The title property provides a localizable display string to be shown in the Metadata panel. Simply
specifying this property makes the field visible.
The dataType property tells the Metadata panel how to display the property, so as to make it editable.
Because this is a simple string value, it will be shown in an editable text field. This property is optional,
and “string” is the default type, so the result is the same if you leave it out.
Setting searchable to true allows you to search for images using this custom metadata field.
9. Add another entry to the table to define a Boolean field. To do this, we will use the enumerated-value
data type.
{
id = 'myboolean',
title = LOC "$$$/MyMetadataSample/Fields/Display=My Boolean",
dataType = 'enum',
values = {
-- add valid-value entries
},
},
10. Now we will limit the possible values to the strings “true” and “false”.
{
id = 'myboolean',
title = LOC "$$$/MyMetadataSample/Fields/Display=My Boolean",
dataType = 'enum',
values = {
{
value = 'true',
title = LOC "$$$/MyMetadataSample/Fields/Display/True=True",
},
{
value = 'false',
title = LOC "$$$/MyMetadataSample/Fields/Display/False=False",
},
},
},
Because we have declared the value type as enum, the Metadata panel displays this field with a pop-up
menu of valid values. Each value has a localizable display string, which appear in the menu. When the
user chooses the menu item, the field is assigned the corresponding string value.
Define a tagset
The drop-down menu at the top left of the Metadata panel allows users to filter what is shown in the
panel, by selecting a metadata tagset to be displayed. There are predefined tagsets, and you can also
create your own. See “Adding custom metadata tagsets” on page 59.
Now that we have defined a set of metadata fields, we will create a tagset for them, so that they can be
selected for display, and displayed together in a labelled section of the Metadata panel. Our tagset will also
include some predefined sets.
items = {
-- add item entries
},
}
➣ The title value is the localizable display string that will show up as the menu item for this tagset.
➣ The items table provides the specific metadata fields to be included in our tagset. We will add
some representative fields. For the complete list of possible field specifiers, see “Defining
metadata fields” on page 55.
3. Add the following entries to the items table, to include a labeled section named Standard Metadata,
which displays the predefined filename and folder metadata fields, part of the built-in metadata for
Lightroom:
items = {
-- add item entries
{ 'com.adobe.label',
label = LOC "$$$/Metadata/OrigLabel=Standard Metadata" },
'com.adobe.filename',
'com.adobe.folder',
'com.adobe.separator',
},
4. Add the entries for the custom metadata defined in this plug-in, in another labeled section:
items = {
{ 'com.adobe.label',
label = LOC "$$$/Metadata/OrigLabel=Standard Metadata" },
'com.adobe.filename',
'com.adobe.folder',
'com.adobe.separator',
The asterisk wild-card character in the field-name part of the path matches all fields defined by this
plug-in. The asterisk can appear only at the end of the field name.
3. Navigate to your new plug-in folder. Check that your plug-in is loaded and running, as shown by a
green traffic-light icon, and the text “Installed and running”. (If it is not, check the Plug-in Author Tools
section of the Plug-in Manager for a diagnostic message.)
5. In Library view, show the Metadata panel and click the name at the top to see the drop-down menu.
Your new tagset, with the label “My Metadata,” should appear at the bottom of the list.
CHAPTER 8: Defining Metadata: A Walkthrough Running the plug-in 175
The Metadata panel should display the filename and folder fields in a section labeled Standard
Metadata, and your custom myString and myBoolean fields in a section labeled My Metadata Sample,
with separators between the sections. The fields are shown with their display labels, and an edit or
selection control.
7. Try editing the custom fields. The myString field, labeled My String, has an editable text field for
setting the value, and the myBoolean field, labeled My Boolean, has a pop-up menu that shows the
allowed values.
Notice that your custom metadata now appears at the bottom of the panel, after all of the standard
metadata.
CHAPTER 8: Defining Metadata: A Walkthrough Customizing the Plug-in Manager 176
Here is an example of adding such a section, using the metadata values we have already defined.
1. In the Info.lua file, add the entry that identifies the Plug-in Info Provider definition script:
return {
LrSdkVersion = 2.0,
LrToolkitIdentifier = 'sample.metadata.mymetadatasample',
LrPluginName = LOC "$$$/MyMetadataSample/PluginName=My Metadata Sample",
LrMetadataProvider = 'MyMetadataDefinitionFile.lua',
LrMetadataTagsetFactory = 'MyMetadataTagset.lua',
LrPluginInfoProvider = 'PluginInfoProvider.lua',
}
2. Add another line that identifies a URL where the user can go for further information about this plug-in:
LrPluginInfoUrl = "http://www.mycompany.com",
This URL will be displayed in the standard Status section of the Plug-in Manager dialog.
3. Create two new files in the plug-in folder named PluginInfoProvider.lua and
PluginManager.lua.
return {
sectionsForTopOfDialog = PluginManager.sectionsForTopOfDialog,
}
CHAPTER 8: Defining Metadata: A Walkthrough Customizing the Plug-in Manager 177
5. The section definition will use variables defined in an initialization script. In the Info.lua file, add the
LrInitPlugin entry that identifies the plug-in initialization script:
return {
LrSdkVersion = 2.0,
LrToolkitIdentifier = 'sample.metadata.mymetadatasample',
LrPluginName = LOC "$$$/MyMetadataSample/PluginName=My Metadata Sample",
LrInitPlugin = 'PluginInit.lua',
LrMetadataProvider = 'MyMetadataDefinitionFile.lua',
LrMetadataTagsetFactory = 'MyMetadataTagset.lua',
LrPluginInfoProvider = 'PluginInfoProvider.lua',
}
6. Create the file PluginInit.lua in the plug-in folder, and edit it to add these variables:
_G.currentDisplayImage = "no"
_G.pluginID = "com.adobe.lightroom.sdk.metadata.custommetadatasample"
_G.URL = "http://www.mycompany.com"
The _G prefix here indicates that these variables are globally available within the plug-in.
7. Edit the file PluginManager.lua to define the function that creates the UI content of the new section.
Notice the use of the variables we defined in the initialization script:
local LrView = import "LrView"
local LrHttp = import "LrHttp"
local bind = import "LrBinding"
local app = import 'LrApplication'
PluginManager = {}
function PluginManager.sectionsForTopOfDialog( f, p )
return {
-- section for the top of the dialog
{
title = "Custom Metadata Sample",
f:row {
spacing = f:control_spacing(),
f:static_text {
title = 'Click the button to find out more about Adobe',
alignment = 'left',
fill_horizontal = 1,
},
f:push_button {
width = 150,
title = 'Connect to Adobe',
enabled = true,
action = function()
LrHttp.openUrlInBrowser(_G.URL)
end,
},
},
f:row {
f:static_text {
title = 'Global default value for displayImage: ',
alignment = 'left',
},
f:static_text {
CHAPTER 8: Defining Metadata: A Walkthrough Customizing the Plug-in Manager 178
title = _G.currentDisplayImage,
fill_horizontal = 1,
},
},
},
}
end
8. Reload and run the plug-in again, as described in “Running the plug-in” on page 174.
When you select the plug-in, the new section appears above the standard Lightroom sections:
custom section
This chapter provides a walkthrough example of how to build a Web Gallery plug-in, which uses a slightly
different architecture from standard export and metadata plug-ins.
This sample code produces a simple HTML gallery that shows a grid of thumbnail images, which respond
to a click by showing a larger version of the clicked image.
These concepts are introduced and explained in detail in Chapter 4, “Writing a Web-engine Plug-in.”
1. Create a single folder, mySamplePlugin.lrwebengine, to hold the plug-in files, in the following folder
according to your operating system:
➣ In Mac OS:
userhome/Library/Application Support/Adobe/Lightroom/
Web Galleries/mySamplePlugin.lrwebengine
➣ In Windows:
LightroomRoot\shared\webengines\mySamplePlugin.lrwebengine
2. In the myWebPlugin folder, create the manifest file that defines the contents of the plug-in, naming it
manifest.lrweb. Include the first command, which specifies a template for a gallery page:
AddGridPages {
template = "grid.html",
rows = 4,
columns = 4,
}
179
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Creating a Web Gallery plug-in 180
2. The two referenced HTML files contain common code for all HTML pages that will be created from this
template. Create these two HTML files in the myWebPlugin folder.
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Adobe Photoshop Lightroom" />
<title>My Sample Plug-in</title>
<link rel="stylesheet" type="text/css" media="screen"
title="Custom Settings" href="$others/custom.css" >
</head>
<body>
Add subfolders
The plug-in folder now contains most of the top-level files for the plug-in:
myWebPlugin/
galleryInfo.lrweb
manifest.lrweb
grid.html
header.html
footer.html
The grid.html page is the template for the thumbnail filmstrip; later we will add a template HTML file for
the large version of the selected thumbnail image.
In addition to these top-level files, the plug-in will require resources of various kinds; default images, style
sheets, JavaScript support code, and string dictionaries for localization.
1. Before going on to flesh out the content of the basic files, add some subfolders to hold resource files:
myWebPlugin/
resources/
css/
js/
strings/
en/
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Defining a data model and functionality 181
model = {
["nonDynamic.imageBase"] = "content",
["photoSizes.thumb.height"] = 150,
["photoSizes.thumb.width"] = 150,
["photoSizes.thumb.metadataExportMode"] = "copyright",
["appearance.thumb.cssID"] = ".thumb",
},
}
This begins to define the parameters for the smallest photo size, naming it “thumb”. The variables
define the images size, allowing us to adjust the number of rows and columns the grid will need to
display them.
2. The model.appearance parameter associates the “thumb” photos with a style-sheet variable. To make
this work, we have to add the style sheet to the project.
3. Now we will add some code to the HTML template that makes use of these variables to display
thumbnail images in the workspace.
In the grid.html file, add this code before the header statement, defining local variables:
<%
--[[ Define some variables to make locating other resources easier. ]]
local mySize = "thumb"
local others = "content"
local theRoot = "."
%>
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Defining a data model and functionality 182
AddCustomCSS {
filename = 'content/custom.css',
}
5. Now we can define the contents of image grid and its contents.
Add this code to grid.html between the header and footer include statements:
<lr:ThumbnailGrid>
<lr:GridPhotoCell>
<img src="$others/bin/images/thumb/<%= image.exportFilename %>.jpg"
id="<%= image.imageID %>" class="thumb" />
</lr:GridPhotoCell>
</lr:ThumbnailGrid>
The logic here retrieves the thumbnail version of an image from the content folder ($others), and
makes it the content of a grid cell. The image name is variable, so each cell shows a different image.
This data model and template define a page that displays thumbnail versions of your images in the Web
Gallery workspace. Next, we will need to allow for more than one page of photos, and add functionality so
that clicking on a thumbnail shows the larger version of the image.
6. In the grid.html page, add this code after the closing ThumbnailGrid tag:
...
</lr:ThumbnailGrid>
<li><a href="$link">Next</a></li>
</lr:NextEnabled>
<lr:NextDisabled>
<li>Next</li>
</lr:NextDisabled>
</lr:Pagination>
</ul>
</div>
<% end %>
The pages make use of the predefined navigation buttons (using the $link variable), associating them
with the text “Previous” and “Next”. This code makes sure that the “Previous” button is disabled on the first
page, and the “Next” button is disabled on the last page.
Each page also displays its own page number (using the $page variable), and allows direct navigation to
other pages (using the $link and $page variables)
➤ Add a link around each thumbnail image that responds to a click by finding and displaying the
corresponding large image.
1. The data model needs to define the new photo size and its supporting parameters.
["photoSizes.large.width"] = 450,
["photoSizes.large.height"] = 450,
["appearance.thumb.cssID"] = ".thumb",
},
2. The project needs to include a template for the frame that displays the large image.
This will create an individual HTML page for each large image, which we can link to from the grid
photo cell definition in grid.html. The name of each page has the text "_large" appended to it; for
example, img0731_large.html.
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Defining a data model and functionality 184
3. In the grid.html template, add a link in each photo cell of the grid that retrieves the large-version
page corresponding to the thumbnail in that cell:
<lr:GridPhotoCell>
<a href="$others/<%= image.exportFilename %>_large.html">
<img src="$others/bin/images/thumb/<%= image.exportFilename %>.jpg"
id="<%= image.imageID %>" class="thumb" />
</a>
</lr:GridPhotoCell>
Notice that the reference to the filename includes the appended text, "_large".
4. Create the new HTML template page, large.html, in the top-level plug-in folder. The new page is
similar to the grid.html page, except that it declares the use of large images, rather than thumbnails,
and there is an image function that retrieves the single image to be shown.
Use the common header and footer code, and define local variables:
<%
--[[ Define some variables to make locating other resources easier.]]
local image = getImage( index )
local theRoot = ".."
local others = "."
local mySize = "large"
%>
5. Add this pagination code after the include-header section. This version includes an Index option
which takes the site back to the grid page:
<div>
<ul>
<lr:Pagination>
<lr:PreviousEnabled>
<li><a href="$link">Previous</a></li>
</lr:PreviousEnabled>
<lr:PreviousDisabled>
<li>Previous</li>
</lr:PreviousDisabled>
<li><a href="$gridPageLink">Index</a></li>
<lr:NextEnabled>
<li><a href="$link">Next</a></li>
</lr:NextEnabled>
<lr:NextDisabled>
<li>Next</li>
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Customizing the Web Gallery UI 185
</lr:NextDisabled>
</lr:Pagination>
</ul>
</div>
6. After the pagination code, add the link that actually retrieves the large image to be shown:
<a href="$gridPageLink">
<img src="bin/images/large/<%= image.exportFilename %>.jpg" />
</a>
To demonstrate these techniques, we will add an entry to the Site Info section of the panel that will allow
users to modify the main title of the page, shown in the main frame. We will allow the user to edit the title
string using a text-edit control in the Site Info section of the main control panel, or using in-place edit in
the preview panel.
The Site Info section of the control panel on the Web Gallery page corresponds to the labels return
value of the views function. We are creating a labeled_text_input control in this section, and
binding its value to the data-model value that holds the site-title text.
CHAPTER 9: Web Gallery Plug-ins: A Tutorial Example Adding a customized tagset 186
3. Edit the header.html template file, adding the following heading immediately following the <body>
tag:
<h1 onclick="clickTarget( this, 'metadata.siteTitle.value' );"
id="metadata.siteTitle.value">$model.metadata.siteTitle.value</h1>
Notice that the title text is found in the model variable, the same one bound to the text-input control
in the control panel. An event handler here allows edit-in-place in the browser window of the Web
Gallery page, in addition to the editing capability provided by UI control.
4. To make edit-in-place work, we need a JavaScript script to handle Live Update. Add the next
statement immediately following, identifying a script to be executed
<script type="text/javascript" src="$theRoot/resources/live_update.js"></script>
5. We need to make this script part of the plug-in. To do this, we need to both provide the script, and tell
the plug-in it’s there.
➣ Create a copy of the file live_update.js, which is part of the Lightroom SDK, and place it in the
resources subfolder of the plug-in. This is a sample implementation of the update functions and
callbacks needed for Live Update.
3. Go to the Web Gallery page and select the new gallery type.
4. Place the cursor over the “MySample” text that appears as the default title; you should be able to edit
it.
5. Look in the Site Info section of the control panel, and try editing the title text from there.
3. Define a function that selects one of the sayings. Make it a global variable that can be referenced from
LuaPage templates:
globals = {
randomSaying = function ()
randomSayingCount = math.mod( randomSayingCount + 1, #sayings )
return sayings[ randomSayingCount ]
end,
}
aQuote = {
startTag = 'write( [[<blockquote style=" margin: 0 0 0 30px;
padding: 10px 0 0 20px; font-size: 88%; line-height: 1.5em;
color: #666;">]] )',
endTag = 'write( [[</blockquote>]] )',
}
}
This defines two dynamic tags with the names saying and aQuote. The tags can be referenced from a
LuaPage template using the prefix with which the tagset is imported, and the tag name in an opening
and closing tag:
<prefix:tagname>...</prefix:tagname>
The inner tag uses the global function we defined to construct some strings containing both static
and dynamic text. These strings are output before and after the text content of the tag. The outer tag
provides some style information for the text.
This associates the prefix “xmpl” with the imported tagset, the tags can be referenced as:
<xmpl:aQuote>
<xmpl:saying>...</xmpl:saying>
</xmpl:aQuote>
6. Finally, we need to use the tags in one of the template pages. Edit the file large.html to add this code
just before the footer:
<xmpl:aQuote>You know what they say:<br>
<xmpl:saying> <br />....how interesting!<br /></xmpl:saying>
</xmpl:aQuote>
7. Save the plug-in and reload it, as described in “Testing the plug-in” on page 186.
At the bottom of the browser, you should now see the constructed text at the bottom, which changes
each time the page is displayed: