diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/Info.lua b/Info.lua new file mode 100644 index 0000000..c786ed1 --- /dev/null +++ b/Info.lua @@ -0,0 +1,16 @@ +return { + LrSdkVersion = 4.0, + LrSdkMinimumVersion = 3.0, -- minimum SDK version required by this plug-in + + LrToolkitIdentifier = 'com.adobe.lightroom.export.stipple', + LrPluginName = LOC "$$$/Stipple/PluginName=Stipple", + + LrExportServiceProvider = { + title = LOC "$$$/Stipple/Stipple-title=Stipple", + file = 'StippleExportServiceProvider.lua', + }, + + LrMetadataProvider = 'StippleMetadataDefinition.lua', + + VERSION = { major=0, minor=1, revision=1, build=1, }, +} \ No newline at end of file diff --git a/StippleAPI.lua b/StippleAPI.lua new file mode 100644 index 0000000..d69a312 --- /dev/null +++ b/StippleAPI.lua @@ -0,0 +1,407 @@ + -- Lightroom SDK +local LrBinding = import 'LrBinding' +local LrDate = import 'LrDate' +local LrDialogs = import 'LrDialogs' +local LrErrors = import 'LrErrors' +local LrFunctionContext = import 'LrFunctionContext' +local LrHttp = import 'LrHttp' +local LrMD5 = import 'LrMD5' +local LrPathUtils = import 'LrPathUtils' +local LrView = import 'LrView' +local LrXml = import 'LrXml' + +local prefs = import 'LrPrefs'.prefsForPlugin() + +local bind = LrView.bind +local share = LrView.share + +local logger = import 'LrLogger'( 'StippleAPI' ) +logger:enable('print') + +local JSON = require 'json' +local urlBase = 'https://stipple.com' + +--============================================================================-- + +StippleAPI = {} + +-------------------------------------------------------------------------------- + +local appearsAlive + +-------------------------------------------------------------------------------- + +local function formatError( nativeErrorCode ) + return LOC "$$$/Stipple/Error/NetworkFailure=Could not contact the Stipple web service. Please check your Internet connection." +end + +-------------------------------------------------------------------------------- + +local function trim( s ) + return string.gsub( s, "^%s*(.-)%s*$", "%1" ) +end + +-------------------------------------------------------------------------------- + +function StippleAPI.showApiKeyDialog( message ) + LrFunctionContext.callWithContext( 'StippleAPI.showApiKeyDialog', function( context ) + local f = LrView.osFactory() + local properties = LrBinding.makePropertyTable( context ) + + properties.apiKey = prefs.apiKey + + local contents = f:column { + bind_to_object = properties, + spacing = f:control_spacing(), + fill = 1, + + f:static_text { + title = LOC "$$$/Stipple/ApiKeyDialog/Message=In order to use the Stipple plug-in, you must obtain an API key from Stipple.com. Sign on to Stipple and register for a key.", + fill_horizontal = 1, + width_in_chars = 55, + height_in_lines = 2, + size = 'small', + }, + + message and f:static_text { + title = message, + fill_horizontal = 1, + width_in_chars = 55, + height_in_lines = 2, + size = 'small', + text_color = import 'LrColor'( 1, 0, 0 ), + } or 'skipped item', + f:row { + spacing = f:label_spacing(), + + f:static_text { + title = LOC "$$$/Stipple/ApiKeyDialog/Key=API Key:", + alignment = 'right', + width = share 'title_width', + }, + + f:edit_field { + fill_horizonal = 1, + width_in_chars = 35, + value = bind 'apiKey', + }, + }, + } + + local result = LrDialogs.presentModalDialog { + title = LOC "$$$/Stipple/ApiKeyDialog/Title=Enter Your Stipple API Key", + contents = contents, + accessoryView = f:push_button { + title = LOC "$$$/Stipple/ApiKeyDialog/GoToStipple=Get Stipple API Key...", + action = function() + LrHttp.openUrlInBrowser( urlBase .. "/api_docs/v1" ) + end + }, + } + + if result == 'ok' then + prefs.apiKey = trim ( properties.apiKey ) + else + LrErrors.throwCanceled() + end + end ) +end + +-------------------------------------------------------------------------------- + +function StippleAPI.getApiKeyAndSecret() + local apiKey = prefs.apiKey + + while not(type( apiKey ) == 'string' and #apiKey > 10) do + local message + + if apiKey then + message = LOC "$$$/Stipple/ApiKeyDialog/Invalid=The key below is not valid." + end + + StippleAPI.showApiKeyDialog( message ) + apiKey = prefs.apiKey + end + + return apiKey +end + +-------------------------------------------------------------------------------- + +function StippleAPI.makeApiSignature( params ) + local apiKey = StippleAPI.getApiKeyAndSecret() + + if not params.api_key then + params.api_key = apiKey + end + + -- Get list of arguments in sorted order. + local argNames = {} + for name in pairs( params ) do + table.insert( argNames, name ) + end + + table.sort( argNames ) + + -- Build the secret string to be MD5 hashed. + local allArgs = sharedSecret + for _, name in ipairs( argNames ) do + if params[ name ] then -- might be false + allArgs = string.format( '%s%s%s', allArgs, name, params[ name ] ) + end + end + + return LrMD5.digest( allArgs ) +end + +-------------------------------------------------------------------------------- + +function StippleAPI.callRestMethod( propertyTable, params ) + local apiKey = StippleAPI.getApiKeyAndSecret() + + if not params.api_key then + params.api_key = apiKey + end + + local suppressError = params.suppressError + local suppressErrorCodes = params.suppressErrorCodes + local skipAuthToken = params.skipAuthToken + + params.suppressError = nil + params.suppressErrorCodes = nil + params.skipAuthToken = nil + + local url = string.format( urlBase .. '/api/v1/%s', assert( params.url ) ) + + for name, value in pairs( params ) do + local query_seperator = '?' + + if name ~= 'url' and value then + local gsubString = '([^0-9A-Za-z])' + + value = tostring( value ) + + if name ~= 'tag_id' then + value = string.gsub( value, gsubString, function( c ) return string.format( '%%%02X', string.byte( c ) ) end ) + end + + value = string.gsub( value, ' ', '+' ) + params[ name ] = value + + url = string.format( '%s%s%s=%s', url, query_seperator, name, value ) + query_seperator = '&' + end + end + + logger:info( 'calling Stipple API via URL:', url ) + local response, hdrs = LrHttp.get( url ) + logger:info( 'Stipple response:', response ) + + if not response then + appearsAlive = false + + if suppressError then + return { stat = "noresponse" } + else + if hdrs and hdrs.error then + LrErrors.throwUserError( formatError( hdrs.error.nativeCode ) ) + end + end + end + + -- Mac has different implementation with that on Windows when the server refuses the request. + if hdrs.status ~= 200 then + LrErrors.throwUserError( formatError( hdrs.status ) ) + end + + appearsAlive = true + + local json = JSON:decode(response) + + if suppressErrorCodes then + local errorCode = simpleXml and simpleXml.err and tonumber( simpleXml.err.code ) + + if errorCode and suppressErrorCodes[ errorCode ] then + suppressError = true + end + end + + if tonumber(json.status) == 200 or suppressError then + logger:info( 'Stipple API returned status ' .. json.status ) + + return json, response + else + logger:warn( 'Stipple API returned error', tostring(json.status) ) + LrErrors.throwUserError( LOC( "$$$/Stipple/Error/API=Stipple API returned an error message (function ^1, status ^2, error ^3)", + tostring(params.url), tostring(json.status), tostring(json.error))) + end +end + +-------------------------------------------------------------------------------- + +function StippleAPI.uploadPhoto( propertyTable, params ) + assert( type( params ) == 'table', 'StippleAPI.uploadPhoto: params must be a table' ) + + local apiKey = StippleAPI.getApiKeyAndSecret() + local postUrl = params.id and urlBase .. '/api/v1/photos/update' or urlBase .. '/api/v1/photos/upload/' + local originalParams = params.id and table.shallowcopy( params ) + + logger:info( 'uploading photo', params.filePath ) + + local filePath = assert( params.filePath ) + params.filePath = nil + + local fileName = LrPathUtils.leafName( filePath ) + local mimeChunks = {} + + for argName, argValue in pairs( params ) do + if argName ~= 'api_key' and argName ~= 'photo' and argValue then + mimeChunks[ #mimeChunks + 1 ] = { name = argName, value = argValue } + end + end + + mimeChunks[ #mimeChunks + 1 ] = { name = 'api_key', value = apiKey } + + if params.photo.caption then + mimeChunks[ #mimeChunks + 1 ] = { name = 'photo[caption]', value = params.photo.caption } + end + + mimeChunks[ #mimeChunks + 1 ] = { name = 'photo[source_page]', value = params.photo.source_page } + mimeChunks[ #mimeChunks + 1 ] = { name = 'file', fileName = fileName, filePath = filePath, contentType = 'application/octet-stream' } + + local response, hdrs = LrHttp.postMultipart( postUrl, mimeChunks ) -- Post it and wait for confirmation. + + if not response then + if hdrs and hdrs.error then + LrErrors.throwUserError( formatError( hdrs.error.nativeCode ) ) + end + end + + -- Parse Stipple response for photo ID. + local json = JSON:decode(response) + + if tonumber(json.status) == 200 then + return json.data.photo.id + elseif params.id and json.error and tonumber(hdrs.error.nativeCode) == 422 then + -- Photo is missing. Most likely, the user deleted it outside of Lightroom. Just repost it. + +-- originalParams.id = nil +-- return StippleAPI.uploadPhoto( propertyTable, originalParams ) + LrErrors.throwUserError( LOC( "$$$/Stipple/Error/API/Upload=Stipple API Falling into the elseif case")) + else + logger:info( 'uploading photo', json.status ) + + LrErrors.throwUserError( LOC( "$$$/Stipple/Error/API/Upload=Stipple API returned an error message (function supload, message ^1)", + tostring( json.status ))) + end +end + +-------------------------------------------------------------------------------- + +function StippleAPI.openAuthUrl() + local response = StippleAPI.callRestMethod( nil, { url = 'users/me' } ) + + return response.data +end + +-------------------------------------------------------------------------------- + +local function getPhotoInfo( propertyTable, params ) + return nil, nil +end + +-------------------------------------------------------------------------------- + +function StippleAPI.constructPhotoURL( propertyTable, params ) + return urlBase .. '/photos/' .. params.id +end + +-------------------------------------------------------------------------------- + +function StippleAPI.constructPhotosetURL( propertyTable, photosetId ) + return urlBase .. "/photos/" .. propertyTable.nsid .. "/sets/" .. photosetId +end + +-------------------------------------------------------------------------------- + +function StippleAPI.constructPhotostreamURL( propertyTable ) + return urlBase .. "/a#library/untagged" +end + +------------------------------------------------------------------------------- + +local function traversePhotosetsForTitle( node, title ) + return '' +end + +-------------------------------------------------------------------------------- + +function StippleAPI.createOrUpdatePhotoset( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +function StippleAPI.listPhotosFromPhotoset( propertyTable, params ) + return nil +end + +-------------------------------------------------------------------------------- + +function StippleAPI.setPhotosetSequence( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +function StippleAPI.addPhotosToSet( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +function StippleAPI.deletePhoto( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +function StippleAPI.deletePhotoset( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +local function removePhotoTags( propertyTable, node, previous_tag ) + return false +end + +-------------------------------------------------------------------------------- + +function StippleAPI.setImageTags( propertyTable, params ) + return true +end + +-------------------------------------------------------------------------------- + +function StippleAPI.getUserInfo( propertyTable, params ) + return { } +end + +-------------------------------------------------------------------------------- + +function StippleAPI.getComments( propertyTable, params ) + return nil +end + +-------------------------------------------------------------------------------- + +function StippleAPI.addComment( propertyTable, params ) + return +end + +-------------------------------------------------------------------------------- + +function StippleAPI.testStippleConnection( propertyTable ) + return true +end \ No newline at end of file diff --git a/StippleExportDialogSections.lua b/StippleExportDialogSections.lua new file mode 100644 index 0000000..e69de29 diff --git a/StippleExportServiceProvider.lua b/StippleExportServiceProvider.lua new file mode 100644 index 0000000..f41ac11 --- /dev/null +++ b/StippleExportServiceProvider.lua @@ -0,0 +1,688 @@ + +-- Lightroom SDK +local LrBinding = import 'LrBinding' +local LrDialogs = import 'LrDialogs' +local LrErrors = import 'LrErrors' +local LrFileUtils = import 'LrFileUtils' +local LrPathUtils = import 'LrPathUtils' +local LrView = import 'LrView' + +local logger = import 'LrLogger'( 'StippleAPI' ) +logger:enable('print') + +-- Common shortcuts +local bind = LrView.bind +local share = LrView.share + +-- JSON Reading/Writing +local JSON = require 'json' + +-- Stipple plug-in +require 'StippleAPI' +require 'StipplePublishSupport' + +local exportServiceProvider = {} + +-- A typical service provider would probably roll all of this into one file, but +-- this approach allows us to document the publish-specific hooks separately. +for name, value in pairs( StipplePublishSupport ) do + exportServiceProvider[ name ] = value +end + +exportServiceProvider.supportsIncrementalPublish = 'only' + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value declares which fields in your property table should + -- be saved as part of an export preset or a publish service connection. If present, + -- should contain an array of items with key and default values. For example: + --
+ -- exportPresetFields = {+ --
+ -- { key = 'username', default = "" },
+ -- { key = 'fullname', default = "" },
+ -- { key = 'nsid', default = "" },
+ -- { key = 'privacy', default = 'public' },
+ -- { key = 'privacy_family', default = false },
+ -- { key = 'privacy_friends', default = false },
+ -- }
+ --
The key
item should match the values used by your user interface
+ -- controls.
The default
item is the value to the first time
+ -- your plug-in is selected in the Export or Publish dialog. On second and subsequent
+ -- activations, the values chosen by the user in the previous session are used.
First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.exportPresetFields + -- @class property + +exportServiceProvider.exportPresetFields = { + { key = 'username', default = "" }, + { key = 'fullname', default = "" }, + { key = 'nsid', default = "" }, +-- { key = 'isUserPro', default = false }, +-- { key = 'auth_token', default = '' }, +-- { 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 = 'addToPhotoset', default = false }, + { key = 'photoset', default = '' }, + { key = 'titleFirstChoice', default = 'title' }, + { key = 'titleSecondChoice', default = 'filename' }, + { key = 'titleRepublishBehavior', default = 'replace' }, +} + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value restricts the display of sections in the Export + -- or Publish dialog to those named. You can use eitherhideSections
or
+ -- showSections
, but not both. If present, this should be an array
+ -- containing one or more of the following strings:
+ -- You cannot suppress display of the "Connection Name" section in the Publish Manager dialog.
+ --If you suppress the "exportLocation" section, the files are rendered into + -- a temporary folder which is deleted immediately after the Export operation + -- completes.
+ --First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.showSections + -- @class property + +--exportServiceProvider.showSections = { 'fileNaming', 'fileSettings', etc... } -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value suppresses the display of the named sections in + -- the Export or Publish dialogs. You can use eitherhideSections
or
+ -- showSections
, but not both. If present, this should be an array
+ -- containing one or more of the following strings:
+ -- You cannot suppress display of the "Connection Name" section in the Publish Manager dialog.
+ --If you suppress the "exportLocation" section, the files are rendered into + -- a temporary folder which is deleted immediately after the Export operation + -- completes.
+ --First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.hideSections + -- @class property + +exportServiceProvider.hideSections = {'exportLocation', 'fileNaming', 'video'} + +-------------------------------------------------------------------------------- +--- (optional, Boolean) If your plug-in allows the display of the exportLocation section, + -- this property controls whether the item "Temporary folder" is available. + -- If the user selects this option, the files are rendered into a temporary location + -- on the hard drive, which is deleted when the export finished. + --If your plug-in hides the exportLocation section, this temporary + -- location behavior is always used.
+ -- @name exportServiceProvider.canExportToTemporaryLocation + -- @class property + +-- exportServiceProvider.canExportToTemporaryLocation = true -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value restricts the available file format choices in the + -- Export or Publish dialogs to those named. You can use eitherallowFileFormats
or
+ -- disallowFileFormats
, but not both. If present, this should be an array
+ -- containing one or more of the following strings:
+ -- This property affects the output of still photo files only;
+ -- it does not affect the output of video files.
+ -- See canExportVideo
.)
First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.allowFileFormats + -- @class property + +exportServiceProvider.allowFileFormats = {'JPEG'} + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value suppresses the named file formats from the list + -- of available file format choices in the Export or Publish dialogs. + -- You can use eitherallowFileFormats
or
+ -- disallowFileFormats
, but not both. If present,
+ -- this should be an array containing one or more of the following strings:
+ -- Affects the output of still photo files only, not video files.
+ -- See canExportVideo
.
First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.disallowFileFormats + -- @class property + +--exportServiceProvider.disallowFileFormats = { 'PSD', 'TIFF', 'DNG', 'ORIGINAL' } -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value restricts the available color space choices in the + -- Export or Publish dialogs to those named. You can use eitherallowColorSpaces
or
+ -- disallowColorSpaces
, but not both. If present, this should be an array
+ -- containing one or more of the following strings:
+ -- Affects the output of still photo files only, not video files.
+ -- See canExportVideo
.
First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.allowColorSpaces + -- @class property + +exportServiceProvider.allowColorSpaces = {'sRGB'} + +-------------------------------------------------------------------------------- +--- (optional) Plug-in defined value suppresses the named color spaces from the list + -- of available color space choices in the Export or Publish dialogs. You can use eitherallowColorSpaces
or
+ -- disallowColorSpaces
, but not both. If present, this should be an array
+ -- containing one or more of the following strings:
+ -- Affects the output of still photo files only, not video files.
+ -- See canExportVideo
.
First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.disallowColorSpaces + -- @class property + + +--exportServiceProvider.disallowColorSpaces = { 'AdobeRGB', 'ProPhotoRGB' } -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional, Boolean) Plug-in defined value is true to hide print resolution controls + -- in the Image Sizing section of the Export or Publish dialog. + -- (Recommended when uploading to most web services.) + --First supported in version 1.3 of the Lightroom SDK.
+ -- @name exportServiceProvider.hidePrintResolution + -- @class property + +exportServiceProvider.hidePrintResolution = true + +-------------------------------------------------------------------------------- +--- (optional, Boolean) When plug-in defined value istrue, both video and + -- still photos can be exported through this plug-in. If not present or set to false, + -- video files cannot be exported through this plug-in. If set to the string "only", + -- video files can be exported, but not still photos. + --No conversions are available for video files. They are simply + -- copied in the same format that was originally imported into Lightroom.
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name exportServiceProvider.canExportVideo + -- @class property + +exportServiceProvider.canExportVideo = false -- video is not supported through this sample plug-in + +-------------------------------------------------------------------------------- +-- FLICKR SPECIFIC: Helper functions and tables. + +local function updateCantExportBecause( propertyTable ) + if not propertyTable.validAccount then + propertyTable.LR_cantExportBecause = LOC "$$$/Stipple/ExportDialog/NoLogin=You haven't logged in to Stipple yet." + return + end + + propertyTable.LR_cantExportBecause = nil +end + +local displayNameForTitleChoice = { + filename = LOC "$$$/Stipple/ExportDialog/Title/Filename=Filename", + title = LOC "$$$/Stipple/ExportDialog/Title/Title=IPTC Title", + empty = LOC "$$$/Stipple/ExportDialog/Title/Empty=Leave Blank", +} + +--local kSafetyTitles = { +-- safe = LOC "$$$/Stipple/ExportDialog/Safety/Safe=Safe", +-- moderate = LOC "$$$/Stipple/ExportDialog/Safety/Moderate=Moderate", +-- restricted = LOC "$$$/Stipple/ExportDialog/Safety/Restricted=Restricted", +--} + +local function booleanToNumber( value ) + return value and 1 or 0 +end + +local privacyToNumber = { + private = 0, + public = 1, +} + +local safetyToNumber = { + safe = 1, + moderate = 2, + restricted = 3, +} + +local contentTypeToNumber = { + photo = 1, + screenshot = 2, + other = 3, +} + +local function getStippleTitle( photo, exportSettings, pathOrMessage ) + local title + + -- Get title according to the options in Stipple Title section. + if exportSettings.titleFirstChoice == 'filename' then + title = LrPathUtils.leafName( pathOrMessage ) + elseif exportSettings.titleFirstChoice == 'title' then + title = photo:getFormattedMetadata 'title' + + if ( not title or #title == 0 ) and exportSettings.titleSecondChoice == 'filename' then + title = LrPathUtils.leafName( pathOrMessage ) + end + end + + return title +end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the + -- user chooses this export service provider in the Export or Publish dialog, + -- or when the destination is already selected when the dialog is invoked, + -- (remembered from the previous export operation). + --This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 1.3 of the Lightroom SDK.
+ -- @param propertyTable (table) An observable table that contains the most + -- recent settings for your export or publish plug-in, including both + -- settings that you have defined and Lightroom-defined export settings + -- @name exportServiceProvider.startDialog + -- @class function + +function exportServiceProvider.startDialog( propertyTable ) + -- Clear login if it's a new connection. + if not propertyTable.LR_editingExistingPublishConnection then + propertyTable.username = nil + propertyTable.nsid = nil + end + + -- Can't export until we've validated the login. + propertyTable:addObserver( 'validAccount', function() updateCantExportBecause( propertyTable ) end ) + updateCantExportBecause( propertyTable ) + + -- Make sure we're logged in. + require 'StippleUser' + StippleUser.verifyLogin( propertyTable ) +end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the user + -- chooses a different export service provider in the Export or Publish dialog + -- or closes the dialog. + --This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 1.3 of the Lightroom SDK.
+ -- @param propertyTable (table) An observable table that contains the most + -- recent settings for your export or publish plug-in, including both + -- settings that you have defined and Lightroom-defined export settings + -- @param why (string) The reason this function was called. One of + -- 'ok', 'cancel', or 'changedServiceProvider' + -- @name exportServiceProvider.endDialog + -- @class function + +--function exportServiceProvider.endDialog( propertyTable ) + -- not used for Stipple plug-in +--end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the user + -- chooses this export service provider in the Export or Publish dialog. + -- It can create new sections that appear above all of the built-in sections + -- in the dialog (except for the Publish Service section in the Publish dialog, + -- which always appears at the very top). + --Your plug-in's startDialog
+ -- function, if any, is called before this function is called.
This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 1.3 of the Lightroom SDK.
+ -- @param f (LrView.osFactory
object)
+ -- A view factory object.
+ -- @param propertyTable (table) An observable table that contains the most
+ -- recent settings for your export or publish plug-in, including both
+ -- settings that you have defined and Lightroom-defined export settings
+ -- @return (table) An array of dialog sections (see example code for details)
+ -- @name exportServiceProvider.sectionsForTopOfDialog
+ -- @class function
+
+function exportServiceProvider.sectionsForTopOfDialog( f, propertyTable )
+ return {
+ {
+ title = LOC "$$$/Stipple/ExportDialog/Account=Stipple Account",
+ synopsis = bind 'accountStatus',
+
+ f:row {
+ spacing = f:control_spacing(),
+
+ f:static_text {
+ title = bind 'accountStatus',
+ alignment = 'right',
+ fill_horizontal = 1,
+ },
+
+ f:push_button {
+ width = tonumber( LOC "$$$/locale_metric/Stipple/ExportDialog/LoginButton/Width=90" ),
+ title = bind 'loginButtonTitle',
+ enabled = bind 'loginButtonEnabled',
+ action = function()
+ require 'StippleUser'
+ StippleUser.login(propertyTable)
+ end,
+ },
+ },
+ },{
+ title = LOC "$$$/Stipple/ExportDialog/Title=Stipple Title",
+ synopsis = function(props)
+ if props.titleFirstChoice == 'title' then
+ return LOC("$$$/Stipple/ExportDialog/Synopsis/TitleWithFallback=IPTC Title or ^1", displayNameForTitleChoice[ props.titleSecondChoice ])
+ else
+ return props.titleFirstChoice and displayNameForTitleChoice[ props.titleFirstChoice ] or ''
+ end
+ end,
+
+ f:column {
+ spacing = f:control_spacing(),
+
+ f:row {
+ spacing = f:label_spacing(),
+
+ f:static_text {
+ title = LOC "$$$/Stipple/ExportDialog/ChooseTitleBy=Set Stipple Title Using:",
+ alignment = 'right',
+ width = share 'stippleTitleSectionLabel',
+ },
+
+ f:popup_menu {
+ value = bind 'titleFirstChoice',
+ width = share 'stippleTitleLeftPopup',
+ items = {
+ { value = 'filename', title = displayNameForTitleChoice.filename },
+ { value = 'title', title = displayNameForTitleChoice.title },
+ { value = 'empty', title = displayNameForTitleChoice.empty },
+ },
+ },
+
+ f:spacer { width = 20 },
+
+ f:static_text {
+ title = LOC "$$$/Stipple/ExportDialog/ChooseTitleBySecondChoice=If Empty, Use:",
+ enabled = LrBinding.keyEquals( 'titleFirstChoice', 'title', propertyTable ),
+ },
+
+ f:popup_menu {
+ value = bind 'titleSecondChoice',
+ enabled = LrBinding.keyEquals( 'titleFirstChoice', 'title', propertyTable ),
+ items = {
+ { value = 'filename', title = displayNameForTitleChoice.filename },
+ { value = 'empty', title = displayNameForTitleChoice.empty },
+ },
+ },
+ },
+
+ f:row {
+ spacing = f:label_spacing(),
+
+ f:static_text {
+ title = LOC "$$$/Stipple/ExportDialog/OnUpdate=When Updating Photos:",
+ alignment = 'right',
+ width = share 'stippleTitleSectionLabel',
+ },
+
+ f:popup_menu {
+ value = bind 'titleRepublishBehavior',
+ width = share 'stippleTitleLeftPopup',
+ items = {
+ { value = 'replace', title = LOC "$$$/Stipple/ExportDialog/ReplaceExistingTitle=Replace Existing Title" },
+ { value = 'leaveAsIs', title = LOC "$$$/Stipple/ExportDialog/LeaveAsIs=Leave Existing Title" },
+ },
+ },
+ },
+ },
+ },
+ }
+end
+
+--------------------------------------------------------------------------------
+--- (optional) This plug-in defined callback function is called when the user
+ -- chooses this export service provider in the Export or Publish dialog.
+ -- It can create new sections that appear below all of the built-in sections in the dialog.
+ -- Your plug-in's startDialog
+ -- function, if any, is called before this function is called.
This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 1.3 of the Lightroom SDK.
+ -- @param f (LrView.osFactory
object)
+ -- A view factory object
+ -- @param propertyTable (table) An observable table that contains the most
+ -- recent settings for your export or publish plug-in, including both
+ -- settings that you have defined and Lightroom-defined export settings
+ -- @return (table) An array of dialog sections (see example code for details)
+ -- @name exportServiceProvider.sectionsForBottomOfDialog
+ -- @class function
+
+--------------------------------------------------------------------------------
+--- (optional) This plug-in defined callback function is called at the beginning
+ -- of each export and publish session before the rendition objects are generated.
+ -- It provides an opportunity for your plug-in to modify the export settings.
+ -- First supported in version 2.0 of the Lightroom SDK.
+ -- @param exportSettings (table) The current export settings. + -- @name exportServiceProvider.updateExportSettings + -- @class function + +--function exportServiceProvider.updateExportSettings( exportSettings ) -- not used for the Stipple sample plug-in +-- exportSettings.LR_format = 'JPEG' +-- exportSettings.LR_jpeg_quality = 100 +-- end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called for each exported photo + -- after it is rendered by Lightroom and after all post-process actions have been + -- applied to it. This function is responsible for transferring the image file + -- to its destination, as defined by your plug-in. The function that + -- you define is launched within a cooperative task that Lightroom provides. You + -- do not need to start your own task to run this function; and in general, you + -- should not need to start another task from within your processing function. + --First supported in version 1.3 of the Lightroom SDK.
+ -- @param functionContext (LrFunctionContext
)
+ -- function context that you can use to attach clean-up behaviors to this
+ -- process; this function context terminates as soon as your function exits.
+ -- @param exportContext (LrExportContext
)
+ -- Information about your export settings and the photos to be published.
+
+function exportServiceProvider.processRenderedPhotos( functionContext, exportContext )
+ local exportSession = exportContext.exportSession -- Make a local reference to the export parameters.
+ local exportSettings = assert( exportContext.propertyTable )
+ local nPhotos = exportSession:countRenditions() -- Get the # of photos.
+
+ -- Set progress title.
+ local progressScope = exportContext:configureProgress {
+ title = nPhotos > 1
+ and LOC( "$$$/Stipple/Publish/Progress=Publishing ^1 photos to Stipple", nPhotos )
+ or LOC "$$$/Stipple/Publish/Progress/One=Publishing one photo to Stipple",
+ }
+
+ local uploadedPhotoIds = {} -- Save off uploaded photo IDs so we can take user to those photos later.
+ local publishedCollectionInfo = exportContext.publishedCollectionInfo
+ local isDefaultCollection = publishedCollectionInfo.isDefaultCollection
+
+ -- Look for a photoset id for this collection.
+ local photosetId = publishedCollectionInfo.remoteId
+
+ -- Get a list of photos already in this photoset so we know which ones we can replace and which have
+ -- to be re-uploaded entirely.
+ local photosetPhotoIds = photosetId and StippleAPI.listPhotosFromPhotoset( exportSettings, { photosetId = photosetId } )
+
+ local photosetPhotosSet = {} -- Turn it into a set for quicker access later.
+
+ if photosetPhotoIds then
+ for _, id in ipairs( photosetPhotoIds ) do
+ photosetPhotosSet[ id ] = true
+ end
+ end
+
+ local couldNotPublishBecauseFreeAccount = {}
+ local stipplePhotoIdsForRenditions = {}
+ local photosetUrl
+
+ for i, rendition in exportContext:renditions { stopIfCanceled = true } do
+ progressScope:setPortionComplete( ( i - 1 ) / nPhotos ) -- Update progress scope.
+
+ local photo = rendition.photo -- Get next photo.
+ local stipplePhotoId = stipplePhotoIdsForRenditions[rendition] -- See if we previously uploaded this photo.
+
+ if not rendition.wasSkipped then
+ local success, pathOrMessage = rendition:waitForRender() -- Update progress scope again once we've got rendered photo.
+
+ progressScope:setPortionComplete( ( i - 0.5 ) / nPhotos ) -- Check for cancellation again after photo has been rendered.
+
+ if progressScope:isCanceled() then break end
+
+ if success then
+ local title = getStippleTitle( photo, exportSettings, pathOrMessage ) -- Build up common metadata for this photo.
+ local description = photo:getFormattedMetadata( 'caption' )
+ local keywordTags = photo:getFormattedMetadata( 'keywordTagsForExport' )
+ local tags
+
+ if keywordTags then
+ tags = {}
+ local keywordIter = string.gfind( keywordTags, "[^,]+" )
+
+ for keyword in keywordIter do
+ if string.sub( keyword, 1, 1 ) == ' ' then
+ keyword = string.sub( keyword, 2, -1 )
+ end
+
+ if string.find( keyword, ' ' ) ~= nil then
+ keyword = '"' .. keyword .. '"'
+ end
+
+ tags[ #tags + 1 ] = keyword
+ end
+ end
+
+ local content_type = contentTypeToNumber[ exportSettings.type ]
+ local previous_tags = photo:getPropertyForPlugin( _PLUGIN, 'previous_tags' )
+ local didReplace = not not stipplePhotoId
+
+ stipplePhotoId = StippleAPI.uploadPhoto(exportSettings, {
+ id = stipplePhotoId,
+ filePath = pathOrMessage,
+ photo = {
+ caption = description, source_page = "Stipple Lightroom Plugin" },
+ claim = 1, -- always claim the photo
+ }
+ )
+
+ if didReplace then
+ -- The replace call used by StippleAPI.uploadPhoto ignores all of the metadata that is passed
+ -- in above. We have to manually upload that info after the fact in this case.
+ if exportSettings.titleRepublishBehavior == 'replace' then
+ --StippleAPI.callRestMethod( exportSettings, {
+ --method = 'stipple.photos.setMeta',
+ --photo_id = stipplePhotoId,
+ --title = title or '',
+ --description = description or '',
+ --})
+ end
+ end
+
+ -- When done with photo, delete temp file. There is a cleanup step that happens later,
+ -- but this will help manage space in the event of a large upload.
+ LrFileUtils.delete( pathOrMessage )
+
+ -- Remember this in the list of photos we uploaded.
+ uploadedPhotoIds[ #uploadedPhotoIds + 1 ] = stipplePhotoId
+
+ -- If this isn't the Photostream, set up the photoset.
+ if not photosetUrl then
+ if not isDefaultCollection then
+ -- Create or update this photoset.
+ photosetUrl = 'https://stipple.com'
+
+ --photosetId, photosetUrl = StippleAPI.createOrUpdatePhotoset(exportSettings, {
+ --photosetId = photosetId,
+ --title = publishedCollectionInfo.name,
+ --description = ??,
+ --primary_photo_id = uploadedPhotoIds[ 1 ],
+ --})
+ else
+ photosetUrl = StippleAPI.constructPhotostreamURL( exportSettings ) -- Photostream: find the URL.
+ end
+ end
+
+ rendition:recordPublishedPhotoId(stipplePhotoId) -- Record this Stipple ID with the photo so we know to replace instead of upload.
+
+ local photoUrl
+
+ if (not isDefaultCollection) then
+ photoUrl = StippleAPI.constructPhotoURL(exportSettings, {
+ id = stipplePhotoId,
+ photosetId = photosetId,
+ })
+
+ -- Add the uploaded photos to the correct photoset.
+ -- StippleAPI.addPhotosToSet(exportSettings, {
+ -- photoId = stipplePhotoId,
+ -- photosetId = photosetId,
+ -- })
+ else
+ photoUrl = StippleAPI.constructPhotoURL(exportSettings, {
+ id = stipplePhotoId,
+ })
+ end
+
+ rendition:recordPublishedPhotoUrl( photoUrl )
+
+ -- Because it is common for Stipple users (even viewers) to add additional tags
+ -- via the Stipple web site, so we can avoid removing those user-added tags that
+ -- were never in Lightroom to begin with. See earlier comment.
+ photo.catalog:withPrivateWriteAccessDo(function()
+ photo:setPropertyForPlugin( _PLUGIN, 'previous_tags', table.concat( tags, ',' ) )
+ end )
+ end
+ else
+ -- To get the skipped photo out of the to-republish bin.
+ rendition:recordPublishedPhotoId(rendition.publishedPhotoId)
+ end
+ end
+
+ if #uploadedPhotoIds > 0 then
+ if (not isDefaultCollection) then
+ --exportSession:recordRemoteCollectionId( photosetId )
+ end
+
+ -- Set up some additional metadata for this collection.
+ exportSession:recordRemoteCollectionUrl( photosetUrl )
+ end
+
+ progressScope:done()
+end
+
+--------------------------------------------------------------------------------
+
+return exportServiceProvider
\ No newline at end of file
diff --git a/StippleLogo.png b/StippleLogo.png
new file mode 100644
index 0000000..f4a2fd3
Binary files /dev/null and b/StippleLogo.png differ
diff --git a/StippleMetadataDefinition.lua b/StippleMetadataDefinition.lua
new file mode 100644
index 0000000..493043b
--- /dev/null
+++ b/StippleMetadataDefinition.lua
@@ -0,0 +1,10 @@
+return {
+ metadataFieldsForPhotos = {
+ {
+ id = 'previous_tags',
+ dataType = 'string',
+ },
+ },
+
+ schemaVersion = 2, -- must be a number, preferably a positive integer
+}
diff --git a/StipplePublishSupport.lua b/StipplePublishSupport.lua
new file mode 100644
index 0000000..3fab2a1
--- /dev/null
+++ b/StipplePublishSupport.lua
@@ -0,0 +1,1480 @@
+-- Lightroom SDK
+local LrDialogs = import 'LrDialogs'
+
+-- Stipple plug-in
+require 'StippleAPI'
+
+--[[
+--- The service definition script for a publish service provider associates
+ -- the code and hooks that extend the behavior of Lightroom's Publish features
+ -- with their implementation for your plug-in. The plug-in's Info.lua
file
+ -- identifies this script in the LrExportServiceProvider
entry. The script
+ -- must define the needed callback functions and properties (with the required
+ -- names and syntax) and assign them to members of the table that it returns.
+ -- The StipplePublishSupport.lua
file of the Stipple sample plug-in provides
+ -- examples of and documentation for the hooks that a plug-in must provide in order to
+ -- define a publish service. Because much of the functionality of a publish service
+ -- is the same as that of an export service, this example builds upon that defined in the
+ -- StippleExportServiceProvider.lua
file.
The service definition script for a publish service should return a table that contains: + --
Most of these functions are the same as those defined for an export service provider.
+ -- Publish services, unlike export services, cannot create presets. (You could think of the
+ -- publish service itself as an export preset.) The settings tables passed
+ -- to these callback functions contain only Lightroom-defined settings, and settings that
+ -- have been explicitly declared in the exportPresetFields
list of the publish service.
+ -- A callback function that you define for a publish service cannot make any changes to the
+ -- settings table passed to it.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.small_icon + -- @class property + +publishServiceProvider.small_icon = 'StippleLogo.png' + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the behavior of the + -- Description entry in the Publish Manager dialog. If the user does not provide + -- an explicit name choice, Lightroom can provide one based on another entry + -- in the publishSettings property table. This entry contains the name of the + -- property that should be used in this case. + -- @name publishServiceProvider.publish_fallbackNameBinding + -- @class property + +publishServiceProvider.publish_fallbackNameBinding = 'fullname' + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- collection to match the terminology used on the service you are targeting. + --This string is typically used in combination with verbs that take action on + -- the published collection, such as "Create ^1" or "Rename ^1".
+ --If not provided, Lightroom uses the default name, "Published Collection."
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedCollection + -- @class property + +publishServiceProvider.titleForPublishedCollection = LOC "$$$/Stipple/TitleForPublishedCollection=Photoset" + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- collection to match the terminology used on the service you are targeting. + --Unlike titleForPublishedCollection
, this string is typically
+ -- used by itself. In English, these strings nay be the same, but in
+ -- other languages (notably German), you may have to use a different form
+ -- of the name to be gramatically correct. If you are localizing your plug-in,
+ -- use a separate translation key to make this possible.
If not provided, Lightroom uses the value of
+ -- titleForPublishedCollection
instead.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedCollection_standalone + -- @class property + +publishServiceProvider.titleForPublishedCollection_standalone = LOC "$$$/Stipple/TitleForPublishedCollection/Standalone=Photoset" + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- collection set to match the terminoy used on the service you are targeting. + --This string is typically used in combination with verbs that take action on + -- the published collection set, such as "Create ^1" or "Rename ^1".
+ --If not provided, Lightroom uses the default name, "Published Collection Set."
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedCollectionSet + -- @class property + +-- publishServiceProvider.titleForPublishedCollectionSet = "(something)" -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- collection to match the terminology used on the service you are targeting. + --Unlike titleForPublishedCollectionSet
, this string is typically
+ -- used by itself. In English, these strings may be the same, but in
+ -- other languages (notably German), you may have to use a different form
+ -- of the name to be gramatically correct. If you are localizing your plug-in,
+ -- use a separate translation key to make this possible.
If not provided, Lightroom uses the value of
+ -- titleForPublishedCollectionSet
instead.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedCollectionSet_standalone + -- @class property + +--publishServiceProvider.titleForPublishedCollectionSet_standalone = "(something)" -- not used for Stipple plug-in + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- smart collection to match the terminology used on the service you are targeting. + --This string is typically used in combination with verbs that take action on + -- the published smart collection, such as "Create ^1" or "Rename ^1".
+ --If not provided, Lightroom uses the default name, "Published Smart Collection."
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedSmartCollection + -- @class property + +publishServiceProvider.titleForPublishedSmartCollection = LOC "$$$/Stipple/TitleForPublishedSmartCollection=Smart Photoset" + +-------------------------------------------------------------------------------- +--- (optional, string) Plug-in defined value customizes the name of a published + -- smart collection to match the terminology used on the service you are targeting. + --Unlike titleForPublishedSmartCollection
, this string is typically
+ -- used by itself. In English, these strings may be the same, but in
+ -- other languages (notably German), you may have to use a different form
+ -- of the name to be gramatically correct. If you are localizing your plug-in,
+ -- use a separate translation key to make this possible.
If not provided, Lightroom uses the value of
+ -- titleForPublishedSmartCollectionSet
instead.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPublishedSmartCollection_standalone + -- @class property + +publishServiceProvider.titleForPublishedSmartCollection_standalone = LOC "$$$/Stipple/TitleForPublishedSmartCollection/Standalone=Smart Photoset" + +-------------------------------------------------------------------------------- +--- (optional) If you provide this plug-in defined callback function, Lightroom calls it to + -- retrieve the default collection behavior for this publish service, then use that information to create + -- a built-in default collection for this service (if one does not yet exist). + -- This special collection is marked in italics and always listed at the top of the list of published collections. + --This callback should return a table that configures the default collection. The + -- elements of the configuration table are optional, and default as shown.
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @return (table) A table with the following fields: + --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForGoToPublishedCollection + -- @class property + +publishServiceProvider.titleForGoToPublishedCollection = LOC "$$$/Stipple/TitleForGoToPublishedCollection=Show in Stipple" + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the user chooses + -- the "Go to Published Collection" context-menu item. + --If this function is not provided, Lightroom uses the URL recorded for the published collection via
+ -- exportSession:recordRemoteCollectionUrl
.
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.goToPublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishedCollectionInfo
)
+ -- An object containing publication information for this published collection.LrPhoto
) The photo object. LrPublishedPhoto
)
+ -- The object that contains information previously recorded about this photo's publication.exportSession:recordRemoteCollectionId
exportSession:recordRemoteCollectionUrl
.First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForGoToPublishedPhoto + -- @class property + +publishServiceProvider.titleForGoToPublishedPhoto = LOC "$$$/Stipple/TitleForGoToPublishedCollection=Show in Stipple" + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the user chooses the + -- "Go to Published Photo" context-menu item. + --If this function is not provided, Lightroom invokes the URL recorded for the published photo via
+ -- exportRendition:recordPublishedPhotoUrl
.
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.goToPublishedPhoto + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishedCollectionInfo
)
+ -- An object containing publication information for this published collection.LrPhoto
) The photo object. LrPublishedPhoto
)
+ -- The object that contains information previously recorded about this photo's publication.exportRendition:recordPublishedPhotoId
exportRendition:recordPublishedPhotoUrl
.This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.didCreateNewPublishService + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.didUpdatePublishService + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.Do not use this hook to actually tear down the service. Instead, use
+ -- willDeletePublishService
+ -- for that purpose.
+ --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.shouldDeletePublishService + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.Do not use this hook to present user interface (aside from progress,
+ -- if the operation will take a long time). Instead, use
+ -- shouldDeletePublishService
+ -- for that purpose.
+ --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.willDeletePublishService + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.Do not use this hook to actually tear down the collection(s). Instead, use
+ -- deletePublishedCollection
+ -- for that purpose.
+ --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.shouldDeletePublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishedCollection
+ -- or LrPublishedCollectionSet
)
+ -- The published collection objects.Do not use this hook to actually delete photo(s). Instead, if the user
+ -- confirms the deletion for all relevant services. Lightroom will call
+ -- deletePhotosFromPublishedCollection
+ -- for that purpose.
+ --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.shouldDeletePhotosFromServiceOnDeleteFromCatalog + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param nPhotos (number) The number of photos that are being deleted. At least + -- one of these photos is published through this service; some may only be published + -- on other services or not published at all. + -- @return (string) What action should Lightroom take? + --deletePhotosFromPublishedCollection
+ -- in this case.)As each photo is deleted, you should call the deletedCallback
+ -- function to inform Lightroom that the deletion was successful. This will cause
+ -- Lightroom to remove the photo from the "Delete Photos to Remove" group in the
+ -- Library grid.
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.deletePhotosFromPublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param arrayOfPhotoIds (table) The remote photo IDs that were declared by this plug-in + -- when they were published. + -- @param deletedCallback (function) This function must be called for each photo ID + -- as soon as the deletion is confirmed by the remote service. It takes a single + -- argument: the photo ID from the arrayOfPhotoIds array. + -- @param localCollectionId (number) The local identifier for the collection for which + -- photos are being removed. + +function publishServiceProvider.deletePhotosFromPublishedCollection( publishSettings, arrayOfPhotoIds, deletedCallback ) + for i, photoId in ipairs( arrayOfPhotoIds ) do + StippleAPI.deletePhoto( publishSettings, { photoId = photoId, suppressErrorCodes = { [ 1 ] = true } } ) -- If Stipple says photo not found, ignore that. + deletedCallback( photoId ) + end +end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called whenever a new + -- publish service is created and whenever the settings for a publish service + -- are changed. It allows the plug-in to specify which metadata should be + -- considered when Lightroom determines whether an existing photo should be + -- moved to the "Modified Photos to Re-Publish" status. + --This is a blocking call.
+ -- @name publishServiceProvider.metadataThatTriggersRepublish + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @return (table) A table containing one or more of the following elements + -- as key, Boolean true or false as a value, where true means that a change + -- to the value does trigger republish status, and false means changes to the + -- value are ignored: + --This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.viewForCollectionSettings + -- @class function + -- @param f (LrView.osFactory
object)
+ -- A view factory object.
+ -- @param publishSettings (table) The settings for this publish service, as specified
+ -- by the user in the Publish Manager dialog. Any changes that you make in
+ -- this table do not persist beyond the scope of this function call.
+ -- @param info (table) A table with these fields:
+ -- LrObservableTable
)
+ -- Plug-in specific settings for this collection. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection. These settings can be accessed via
+ -- LrPublishedCollection:getCollectionInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.
+ -- There are some special properties in this table:
+ -- LR_canSaveCollection
,
+ -- which allows you to disable the Edit or Create button in the collection dialog.
+ -- (If set to true, the Edit / Create button is enabled; if false, it is disabled.)
LR_liveName
will be kept current with the value displayed
+ -- in the name field of the dialog during the life span of the dialog. This enables
+ -- a plug-in to add an observer to monitor name changes performed in the dialog.
LR_canEditName
allows the plug-in
+ -- to control whether the edit field containing the collection name in the dialog is enabled.
+ -- In the case of new creation, the value defaults to true, meaning that the collection name is
+ -- editable via the UI, while in the case of a collection being edited, the value defaults in accordance with
+ -- what the plug-in specifies (or doesn't specify) via 'publishServiceProvider.disableRenamePublishedCollection'.
viewForCollectionSetSettings
)LrObservableTable
)
+ -- This is a place for your plug-in to store transient state while the collection
+ -- settings dialog is running. It is passed to your plug-in's
+ -- endDialogForCollectionSettings
callback, and then discarded.LrPublishedCollection
)
+ -- The published collection object being edited, or nil when creating a new
+ -- collection.LrPublishService
)
+ -- The publish service object to which this collection belongs.f:groupBox
be the outermost view.)
+
+--[[ Not used for Stipple plug-in. This is an example of how this function might work.
+
+function publishServiceProvider.viewForCollectionSettings( f, publishSettings, info )
+
+ local collectionSettings = assert( info.collectionSettings )
+
+ -- Fill in default parameters. This code sample targets a hypothetical service
+ -- that allows users to enable or disable ratings and comments on a per-collection
+ -- basis.
+
+ if collectionSettings.enableRating == nil then
+ collectionSettings.enableRating = false
+ end
+
+ if collectionSettings.enableComments == nil then
+ collectionSettings.enableComments = false
+ end
+
+ local bind = import 'LrView'.bind
+
+ return f:group_box {
+ title = "Sample Plug-in Collection Settings", -- this should be localized via LOC
+ size = 'small',
+ fill_horizontal = 1,
+ bind_to_object = assert( collectionSettings ),
+
+ f:column {
+ fill_horizontal = 1,
+ spacing = f:label_spacing(),
+
+ f:checkbox {
+ title = "Enable Rating", -- this should be localized via LOC
+ value = bind 'enableRating',
+ },
+
+ f:checkbox {
+ title = "Enable Comments", -- this should be localized via LOC
+ value = bind 'enableComments',
+ },
+ },
+
+ }
+
+end
+--]]
+
+--------------------------------------------------------------------------------
+--- (optional) This plug-in defined callback function is called when the user
+ -- creates a new published collection set or edits an existing one. It can add
+ -- additional controls to the dialog box for editing this collection set. These controls
+ -- can be used to configure behaviors specific to this collection set (such as
+ -- privacy or appearance on a web service).
+ -- This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.viewForCollectionSetSettings + -- @class function + -- @param f (LrView.osFactory
object)
+ -- A view factory object.
+ -- @param publishSettings (table) The settings for this publish service, as specified
+ -- by the user in the Publish Manager dialog. Any changes that you make in
+ -- this table do not persist beyond the scope of this function call.
+ -- @param info (table) A table with these fields:
+ -- LrObservableTable
)
+ -- plug-in specific settings for this collection set. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection set. These settings can be accessed via
+ -- LrPublishedCollection:getCollectionSetInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.
+ -- There are some special properties in this table:
+ -- LR_canSaveCollection
,
+ -- which allows you to disable the Edit or Create button in the collection set dialog.
+ -- (If set to true, the Edit / Create button is enabled; if false, it is disabled.)
LR_liveName
will be kept current with the value displayed
+ -- in the name field of the dialog during the life span of the dialog. This enables
+ -- a plug-in to add an observer to monitor name changes performed in the dialog.
LR_canEditName
allows the plug-in
+ -- to control whether the edit field containing the collection set name in the dialog is enabled.
+ -- In the case of new creation, the value defaults to true, meaning that the collection set name is
+ -- editable via the UI, while in the case of a collection being edited, the value defaults in accordance with
+ -- what the plug-in specifies (or doesn't specify) via 'publishServiceProvider.disableRenamePublishedCollection'.
LrObservableTable
)
+ -- This is a place for your plug-in to store transient state while the collection set
+ -- settings dialog is running. It will be passed to your plug-in during the
+ -- endDialogForCollectionSettings
and then discarded.LrPublishedCollectionSet
)
+ -- The published collection set object being edited. Will be nil when creating a new
+ -- collection Set.LrPublishService
)
+ -- The publish service object.f:groupBox
be the outermost view.)
+
+--[[ Not used for Stipple plug-in.
+
+function publishServiceProvider.viewForCollectionSetSettings( f, publishSettings, info )
+ -- See viewForCollectionSettings example above.
+end
+
+--]]
+
+--------------------------------------------------------------------------------
+--- (optional) This plug-in defined callback function is called when the user
+ -- closes the dialog for creating a new published collection or editing an existing
+ -- one. It is only called if you have also provided the viewForCollectionSettings
+ -- callback, and is your opportunity to clean up any tasks or processes you may
+ -- have started while the dialog was running.
+ -- This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
Your code should not update the server from here. That should be done
+ -- via the updateCollectionSettings
callback. (If, for instance, the
+ -- settings changes are later undone; this callback is not called again, but
+ -- updateCollectionSettings
is.)
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.endDialogForCollectionSettings + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrObservableTable
)
+ -- Plug-in specific settings for this collection. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection. These settings can be accessed via
+ -- LrPublishedCollection:getCollectionInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.LrObservableTable
)
+ -- This is a place for your plug-in to store transient state while the collection
+ -- settings dialog is running. It is passed to your plug-in's
+ -- endDialogForCollectionSettings
callback, and then discarded.LrPublishedCollection
)
+ -- The published collection object being edited.LrPublishService
)
+ -- The publish service object to which this collection belongs.viewForCollectionSetSettings
+ -- callback, and is your opportunity to clean up any tasks or processes you may
+ -- have started while the dialog was running.
+ -- This is a blocking call. If you need to start a long-running task (such as
+ -- network access), create a task using the LrTasks
+ -- namespace.
Your code should not update the server from here. That should be done
+ -- via the updateCollectionSetSettings
callback. (If, for instance, the
+ -- settings changes are later undone; this callback will not be called again;
+ -- updateCollectionSetSettings
will be.)
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.endDialogForCollectionSetSettings + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrObservableTable
)
+ -- plug-in specific settings for this collection set. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection set. These settings can be accessed via
+ -- LrPublishedCollectionSet:getCollectionSetInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.LrObservableTable
)
+ -- This is a place for your plug-in to store transient state while the collection set
+ -- settings dialog is running. It will be passed to your plug-in during the
+ -- endDialogForCollectionSettings
and then discarded.LrPublishedCollectionSet
)
+ -- The published collection set object being edited.LrPublishService
)
+ -- The publish service object.viewForCollectionSettings
+ -- callback. It is your opportunity to update settings on your web service to
+ -- match the new settings.
+ -- This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
Your code should not use this callback function to clean up from the + -- dialog. This callback is not be called if the user cancels the dialog.
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.updateCollectionSettings + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrObservableTable
)
+ -- Plug-in specific settings for this collection. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection. These settings can be accessed via
+ -- LrPublishedCollection:getCollectionInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.
+ -- LrPublishedCollection
+ -- or LrPublishedCollectionSet
)
+ -- The published collection object being edited.LrPublishService
)
+ -- The publish service object to which this collection belongs.viewForCollectionSetSettings
+ -- callback. It is your opportunity to update settings on your web service to
+ -- match the new settings.
+ -- This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
Your code should not use this callback function to clean up from the + -- dialog. This callback will not be called if the user cancels the dialog.
+ --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.updateCollectionSetSettings + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrObservableTable
)
+ -- Plug-in specific settings for this collection set. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection set. These settings can be accessed via
+ -- LrPublishedCollectionSet:getCollectionSetInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.
+ -- LrPublishedCollectionSet
)
+ -- The published collection set object being edited.LrPublishService
)
+ -- The publish service object.This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
LrPublishedCollectionInfo
) an object containing publication information for this published collection.
+ -- @return (boolean) true to reverse the sequence when publishing new photos
+
+function publishServiceProvider.shouldReverseSequenceForPublishedCollection( publishSettings, collectionInfo )
+ return false
+end
+
+--------------------------------------------------------------------------------
+--- (Boolean) If this plug-in defined property is set to true, Lightroom will
+ -- enable collections from this service to be sorted manually and will call
+ -- the imposeSortOrderOnPublishedCollection
+ -- callback to cause photos to be sorted on the service after each Publish
+ -- cycle.
+ -- @name publishServiceProvider.supportsCustomSortOrder
+ -- @class property
+
+publishServiceProvider.supportsCustomSortOrder = true
+
+--------------------------------------------------------------------------------
+--- (optional) This plug-in defined callback function is called after each time
+ -- that photos are published via this service assuming the published collection
+ -- is set to "User Order." Your plug-in should ensure that the photos are displayed
+ -- in the designated sequence on the service.
+ -- This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
LrObservableTable
)
+ -- plug-in specific settings for this collection set. The settings in this table
+ -- are not interpreted by Lightroom in any way, except that they are stored
+ -- with the collection set. These settings can be accessed via
+ -- LrPublishedCollectionSet:getCollectionSetInfoSummary
.
+ -- The values in this table must be numbers, strings, or Booleans.
+ -- exportSession:recordRemoteCollectionId
exportSession:recordRemoteCollectionUrl
.exportRendition:recordPublishedPhotoId
+ -- @return (boolean) true to reverse the sequence when publishing new photos
+
+function publishServiceProvider.imposeSortOrderOnPublishedCollection( publishSettings, info, remoteIdSequence )
+ local photosetId = info.remoteCollectionId
+
+ if photosetId then
+ -- Get existing list of photos from the photoset. We want to be sure that we don't
+ -- remove photos that were posted to this photoset by some other means by doing
+ -- this call, so we look for photos that were missed and reinsert them at the end.
+ local existingPhotoSequence = StippleAPI.listPhotosFromPhotoset(publishSettings, { photosetId = photosetId })
+ -- Make a copy of the remote sequence from LR and then tack on any photos we didn't see earlier.
+ local combinedRemoteSequence = {}
+ local remoteIdsInSequence = {}
+
+ for i, id in ipairs( remoteIdSequence ) do
+ combinedRemoteSequence[ i ] = id
+ remoteIdsInSequence[ id ] = true
+ end
+
+ for _, id in ipairs( existingPhotoSequence ) do
+ if not remoteIdsInSequence[ id ] then
+ combinedRemoteSequence[ #combinedRemoteSequence + 1 ] = id
+ end
+ end
+
+ -- There may be no photos left in the set, so check for that before trying
+ -- to set the sequence.
+ if existingPhotoSequence and existingPhotoSequence.primary then
+ StippleAPI.setPhotosetSequence(publishSettings, {
+ photosetId = photosetId,
+ primary = existingPhotoSequence.primary,
+ photoIds = combinedRemoteSequence } )
+ end
+ end
+end
+
+-------------------------------------------------------------------------------
+--- This plug-in defined callback function is called when the user attempts to change the name
+ -- of a collection, to validate that the new name is acceptable for this service.
+ -- This is a blocking call. You should use it only to validate easily-verified + -- characteristics of the name, such as illegal characters in the name. For + -- characteristics that require validation against a server (such as duplicate + -- names), you should accept the name here and reject the name when the server-side operation + -- is attempted.
+ -- @name publishServiceProvider.validatePublishedCollectionName + -- @class function + -- @param proposedName (string) The name as currently typed in the new/rename/edit + -- collection dialog. + -- @return (Boolean) True if the name is acceptable, false if not + -- @return (string) If the name is not acceptable, a string that describes the reason, suitable for display. + +--[[ Not used for Stipple plug-in. + +function publishServiceProvider.validatePublishedCollectionName( proposedName ) + return true +end + +--]] + +------------------------------------------------------------------------------- +--- (Boolean) This plug-in defined value, when true, disables (dims) the Rename Published + -- Collection command in the context menu of the Publish Services panel + -- for all published collections created by this service. + --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.disableRenamePublishedCollection + -- @class property + +-- publishServiceProvider.disableRenamePublishedCollection = true -- not used for Stipple sample plug-in + +------------------------------------------------------------------------------- +--- (Boolean) This plug-in defined value, when true, disables (dims) the Rename Published + -- Collection Set command in the context menu of the Publish Services panel + -- for all published collection sets created by this service. + --First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.disableRenamePublishedCollectionSet + -- @class property + +-- publishServiceProvider.disableRenamePublishedCollectionSet = true -- not used for Stipple sample plug-in + +------------------------------------------------------------------------------- +--- This plug-in callback function is called when the user has renamed a + -- published collection via the Publish Services panel user interface. This is + -- your plug-in's opportunity to make the corresponding change on the service. + --If your plug-in is unable to update the remote service for any reason, + -- you should throw a Lua error from this function; this causes Lightroom to revert the change.
+ --This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.renamePublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.LrPublishedCollection
+ -- or LrPublishedCollectionSet
)
+ -- The published collection object being renamed.exportSession:recordRemoteCollectionId
exportSession:recordRemoteCollectionUrl
.If your plug-in is unable to update the remote service for any reason, + -- you should throw a Lua error from this function; this causes Lightroom to revert the change.
+ --This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.reparentPublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.LrPublishedCollection
+ -- or LrPublishedCollectionSet
)
+ -- The published collection object being renamed.exportSession:recordRemoteCollectionId
exportSession:recordRemoteCollectionUrl
.If your plug-in is unable to update the remote service for any reason, + -- you should throw a Lua error from this function; this causes Lightroom to revert the change.
+ --This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.deletePublishedCollection + -- @class function + -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param info (table) A table with these fields: + --LrPublishService
)
+ -- The publish service object.LrPublishedCollection
+ -- or LrPublishedCollectionSet
)
+ -- The published collection object being renamed.exportSession:recordRemoteCollectionId
exportSession:recordRemoteCollectionUrl
.This function is not called for unpublished photos or collections that do not contain any published photos.
+ --The body of this function should have a loop that looks like this:
+ --+ -- function publishServiceProvider.getCommentsFromPublishedCollection( settings, arrayOfPhotoInfo, commentCallback )+ --
+ --
+ -- for i, photoInfo in ipairs( arrayOfPhotoInfo ) do
+ --
+ -- -- Get comments from service.
+ --
+ -- local comments = (depends on your plug-in's service)
+ --
+ -- -- Convert comments to Lightroom's format.
+ --
+ -- local commentList = {}
+ -- for i, comment in ipairs( comments ) do
+ -- table.insert( commentList, {
+ -- commentId = (comment ID, if any, from service),
+ -- commentText = (text of user comment),
+ -- dateCreated = (date comment was created, if available; Cocoa date format),
+ -- username = (user ID, if any, from service),
+ -- realname = (user's actual name, if available),
+ -- } )
+ --
+ -- end
+ --
+ -- -- Call Lightroom's callback function to register comments.
+ --
+ -- commentCallback { publishedPhoto = photoInfo, comments = commentList }
+ --
+ -- end
+ --
+ -- end + --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param arrayOfPhotoInfo (table) An array of tables with a member table for each photo. + -- Each member table has these fields: + --LrPhoto
) The photo object.LrPublishedPhoto
)
+ -- The publishing data for that photo.getRatingsFromPublishedCollection
.
+ -- First supported in version 3.0 of the Lightroom SDK.
+ -- @name publishServiceProvider.titleForPhotoRating + -- @class property + +publishServiceProvider.titleForPhotoRating = LOC "$$$/Stipple/TitleForPhotoRating=Favorite Count" + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called (if supplied) + -- to retrieve ratings from the remote service, for a single collection of photos + -- that have been published through this service. This function is called: + --The body of this function should have a loop that looks like this:
+ --+ -- function publishServiceProvider.getRatingsFromPublishedCollection( settings, arrayOfPhotoInfo, ratingCallback )+ --
+ --
+ -- for i, photoInfo in ipairs( arrayOfPhotoInfo ) do
+ --
+ -- -- Get ratings from service.
+ --
+ -- local ratings = (depends on your plug-in's service)
+ -- -- WARNING: The value for ratings must be a single number.
+ -- -- This number is displayed in the Comments panel, but is not
+ -- -- otherwise parsed by Lightroom.
+ --
+ -- -- Call Lightroom's callback function to register rating.
+ --
+ -- ratingCallback { publishedPhoto = photoInfo, rating = rating }
+ --
+ -- end
+ --
+ -- end + --
This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param arrayOfPhotoInfo (table) An array of tables with a member table for each photo. + -- Each member table has these fields: + --LrPhoto
) The photo object.LrPublishedPhoto
)
+ -- The publishing data for that photo.This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @return (Boolean) True if comments can be added at this time. + +function publishServiceProvider.canAddCommentsToService( publishSettings ) + return StippleAPI.testStippleConnection( publishSettings ) +end + +-------------------------------------------------------------------------------- +--- (optional) This plug-in defined callback function is called when the user adds + -- a new comment to a published photo in the Library module's Comments panel. + -- Your implementation should publish the comment to the service. + --This is not a blocking call. It is called from within a task created
+ -- using the LrTasks
namespace. In most
+ -- cases, you should not need to start your own task within this function.
First supported in version 3.0 of the Lightroom SDK.
+ -- @param publishSettings (table) The settings for this publish service, as specified + -- by the user in the Publish Manager dialog. Any changes that you make in + -- this table do not persist beyond the scope of this function call. + -- @param remotePhotoId (string or number) The remote ID of the photo as previously assigned + -- via a call toexportRendition:recordRemotePhotoId()
.
+ -- @param commentText (string) The text of the new comment.
+ -- @return (Boolean) True if comment was successfully added to service.
+
+function publishServiceProvider.addCommentToPublishedPhoto( publishSettings, remotePhotoId, commentText )
+ local success = StippleAPI.addComment(publishSettings, {
+ photoId = remotePhotoId,
+ commentText = commentText,
+ })
+
+ return success
+end
+
+--------------------------------------------------------------------------------
+
+StipplePublishSupport = publishServiceProvider
\ No newline at end of file
diff --git a/StippleUser.lua b/StippleUser.lua
new file mode 100644
index 0000000..817c50b
--- /dev/null
+++ b/StippleUser.lua
@@ -0,0 +1,158 @@
+-- Lightroom SDK
+local LrDialogs = import 'LrDialogs'
+local LrFunctionContext = import 'LrFunctionContext'
+local LrTasks = import 'LrTasks'
+local logger = import 'LrLogger'( 'StippleAPI' )
+
+require 'StippleAPI'
+
+--============================================================================--
+
+StippleUser = {}
+
+--------------------------------------------------------------------------------
+
+local function storedCredentialsAreValid( propertyTable )
+ return propertyTable.username and string.len( propertyTable.username ) > 0
+ and propertyTable.nsid
+end
+
+--------------------------------------------------------------------------------
+
+local function notLoggedIn( propertyTable )
+ propertyTable.token = nil
+ propertyTable.nsid = nil
+ propertyTable.username = nil
+ propertyTable.fullname = ''
+
+ propertyTable.accountStatus = LOC "$$$/Stipple/AccountStatus/NotLoggedIn=Not logged in"
+ propertyTable.loginButtonTitle = LOC "$$$/Stipple/LoginButton/NotLoggedIn=Log In"
+ propertyTable.loginButtonEnabled = true
+ propertyTable.validAccount = false
+end
+
+--------------------------------------------------------------------------------
+
+local doingLogin = false
+
+function StippleUser.login( propertyTable )
+ if doingLogin then return end
+ doingLogin = true
+
+ LrFunctionContext.postAsyncTaskWithContext('Stipple login', function(context)
+ if not propertyTable.LR_editingExistingPublishConnection then
+ notLoggedIn( propertyTable )
+ end
+
+ propertyTable.accountStatus = LOC "$$$/Stipple/AccountStatus/LoggingIn=Logging in..."
+ propertyTable.loginButtonEnabled = false
+
+ LrDialogs.attachErrorDialogToFunctionContext(context)
+
+ context:addCleanupHandler(function()
+ doingLogin = false
+
+ if not storedCredentialsAreValid( propertyTable ) then
+ notLoggedIn( propertyTable )
+ end
+ end )
+
+ StippleAPI.getApiKeyAndSecret()
+ propertyTable.accountStatus = LOC "$$$/Stipple/AccountStatus/WaitingForStipple=Waiting for response from stipple.com..."
+
+ local auth = StippleAPI.openAuthUrl()
+ propertyTable.accountStatus = LOC "$$$/Stipple/AccountStatus/WaitingForStipple=Waiting for response from stipple.com..."
+
+ if propertyTable.LR_editingExistingPublishConnection then
+ if auth.user and propertyTable.nsid ~= auth.user.id then
+ LrDialogs.message( LOC "$$$/Stipple/CantChangeUserID=You can not change Stipple accounts on an existing publish connection. Please log in again with the account you used when you first created this connection." )
+ return
+ end
+ end
+
+ propertyTable.nsid = auth.user.id
+ propertyTable.username = auth.user.login
+ propertyTable.fullname = auth.user.name
+
+ StippleUser.updateUserStatusTextBindings( propertyTable )
+ end )
+end
+
+--------------------------------------------------------------------------------
+
+local function getDisplayUserNameFromProperties(propertyTable)
+ local displayUserName = propertyTable.fullname
+
+ if ( not displayUserName or #displayUserName == 0 )
+ or displayUserName == propertyTable.username
+ then
+ displayUserName = propertyTable.username
+ else
+ displayUserName = LOC("$$$/Stipple/AccountStatus/UserNameAndLoginName=^1 (^2)", propertyTable.fullname, propertyTable.username)
+ end
+
+ return displayUserName
+end
+
+--------------------------------------------------------------------------------
+
+function StippleUser.verifyLogin(propertyTable)
+ local function updateStatus()
+ logger:trace( "verifyLogin: updateStatus() was triggered." )
+
+ LrTasks.startAsyncTask(function()
+ logger:trace( "verifyLogin: updateStatus() is executing." )
+ if storedCredentialsAreValid(propertyTable) then
+ local displayUserName = getDisplayUserNameFromProperties(propertyTable)
+
+ propertyTable.accountStatus = LOC( "$$$/Stipple/AccountStatus/LoggedIn=Logged in as ^1", displayUserName )
+
+ if propertyTable.LR_editingExistingPublishConnection then
+ propertyTable.loginButtonTitle = LOC "$$$/Stipple/LoginButton/LogInAgain=Log In"
+ propertyTable.loginButtonEnabled = false
+ propertyTable.validAccount = true
+ else
+ propertyTable.loginButtonTitle = LOC "$$$/Stipple/LoginButton/LoggedIn=Switch User?"
+ propertyTable.loginButtonEnabled = true
+ propertyTable.validAccount = true
+ end
+ else
+ notLoggedIn(propertyTable)
+ end
+
+ StippleUser.updateUserStatusTextBindings(propertyTable)
+ end )
+ end
+
+ propertyTable:addObserver('nsid', updateStatus)
+ updateStatus()
+end
+
+--------------------------------------------------------------------------------
+
+function StippleUser.updateUserStatusTextBindings(settings)
+ local nsid = settings.id
+
+ if nsid and string.len(nsid) > 0 then
+ LrFunctionContext.postAsyncTaskWithContext('Stipple account status check', function(context)
+ context:addFailureHandler(function()
+ if settings.LR_editingExistingPublishConnection then
+ local displayUserName = getDisplayUserNameFromProperties( settings )
+
+ settings.accountStatus = LOC( "$$$/Stipple/AccountStatus/LogInFailed=Log in failed, was logged in as ^1", displayUserName )
+ settings.loginButtonTitle = LOC "$$$/Stipple/LoginButton/LogInAgain=Log In"
+ settings.loginButtonEnabled = true
+ settings.validAccount = false
+ settings.isUserPro = false
+ settings.accountTypeMessage = LOC "$$$/Stipple/AccountStatus/LoginFailed/Message=Could not verify this Stipple account. Please log in again. Please note that you can not change the Stipple account for an existing publish connection. You must log in to the same account."
+ end
+ end )
+
+ settings.accountTypeMessage = LOC( "$$$/Stipple/ProAccountDescription=This Stipple Pro account can utilize collections, modified photos will be automatically be re-published, and there is no monthly bandwidth limit." )
+ settings.isUserPro = true
+ end )
+ else
+ settings.accountTypeMessage = LOC( "$$$/Stipple/SignIn=Sign in with your Stipple account." )
+ settings.isUserPro = false
+ end
+end
\ No newline at end of file
diff --git a/json.lua b/json.lua
new file mode 100644
index 0000000..f38ea9d
--- /dev/null
+++ b/json.lua
@@ -0,0 +1,861 @@
+-- -*- coding: utf-8 -*-
+--
+-- Copyright 2010-2013 Jeffrey Friedl
+-- http://regex.info/blog/
+--
+-- Latest copy: http://regex.info/blog/lua/json
+--
+local VERSION = 20130120.6 -- version history at end of file
+local OBJDEF = { VERSION = VERSION }
+
+--
+-- Simple JSON encoding and decoding in pure Lua.
+-- http://www.json.org/
+--
+--
+-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
+--
+-- local lua_value = JSON:decode(raw_json_text)
+--
+-- local raw_json_text = JSON:encode(lua_table_or_value)
+-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
+--
+--
+-- DECODING
+--
+-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
+--
+-- local lua_value = JSON:decode(raw_json_text)
+--
+-- If the JSON text is for an object or an array, e.g.
+-- { "what": "books", "count": 3 }
+-- or
+-- [ "Larry", "Curly", "Moe" ]
+--
+-- the result is a Lua table, e.g.
+-- { what = "books", count = 3 }
+-- or
+-- { "Larry", "Curly", "Moe" }
+--
+--
+-- The encode and decode routines accept an optional second argument, "etc", which is not used
+-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any
+-- type (including nil).
+--
+-- With most errors during decoding, this code calls
+--
+-- JSON:onDecodeError(message, text, location, etc)
+--
+-- with a message about the error, and if known, the JSON text being parsed and the byte count
+-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your
+-- own function.
+--
+-- The default onDecodeError() merely augments the message with data about the text and the
+-- location if known (and if a second 'etc' argument had been provided to decode(), its value is
+-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's
+-- built-in assert(), and can also be overridden.
+--
+-- For example, in an Adobe Lightroom plugin, you might use something like
+--
+-- function JSON:onDecodeError(message, text, location, etc)
+-- LrErrors.throwUserError("Internal Error: invalid JSON data")
+-- end
+--
+-- or even just
+--
+-- function JSON.assert(message)
+-- LrErrors.throwUserError("Internal Error: " .. message)
+-- end
+--
+-- If JSON:decode() is passed a nil, this is called instead:
+--
+-- JSON:onDecodeOfNilError(message, nil, nil, etc)
+--
+-- and if JSON:decode() is passed HTML instead of JSON, this is called:
+--
+-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
+--
+-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error
+-- reporting, especially when you provide your own error-handling routines. Continuing with the
+-- the Adobe Lightroom plugin example:
+--
+-- function JSON:onDecodeError(message, text, location, etc)
+-- local note = "Internal Error: invalid JSON data"
+-- if type(etc) = 'table' and etc.photo then
+-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName')
+-- end
+-- LrErrors.throwUserError(note)
+-- end
+--
+-- :
+-- :
+--
+-- for i, photo in ipairs(photosToProcess) do
+-- :
+-- :
+-- local data = JSON:decode(someJsonText, { photo = photo })
+-- :
+-- :
+-- end
+--
+--
+--
+--
+
+-- DECODING AND STRICT TYPES
+--
+-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally
+-- possible to tell which a Lua table came from, or guarantee decode-encode round-trip
+-- equivalency.
+--
+-- However, if you enable strictTypes, e.g.
+--
+-- JSON = (loadfile "JSON.lua")() --load the routines
+-- JSON.strictTypes = true
+--
+-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua
+-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type.
+--
+-- (This is not the default because other routines may not work well with tables that have a
+-- metatable set, for example, Lightroom API calls.)
+--
+--
+-- ENCODING
+--
+-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
+--
+-- local raw_json_text = JSON:encode(lua_table_or_value)
+-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
+
+-- On error during encoding, this code calls:
+--
+-- JSON:onEncodeError(message, etc)
+--
+-- which you can override in your local JSON object.
+--
+--
+-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT
+--
+-- assert
+-- onDecodeError
+-- onDecodeOfNilError
+-- onDecodeOfHTMLError
+-- onEncodeError
+--
+-- If you want to create a separate Lua JSON object with its own error handlers,
+-- you can reload JSON.lua or use the :new() method.
+--
+---------------------------------------------------------------------------
+
+
+local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-"
+local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray
+local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
+
+
+function OBJDEF:newArray(tbl)
+ return setmetatable(tbl or {}, isArray)
+end
+
+function OBJDEF:newObject(tbl)
+ return setmetatable(tbl or {}, isObject)
+end
+
+local function unicode_codepoint_as_utf8(codepoint)
+ --
+ -- codepoint is a number
+ --
+ if codepoint <= 127 then
+ return string.char(codepoint)
+
+ elseif codepoint <= 2047 then
+ --
+ -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8
+ --
+ local highpart = math.floor(codepoint / 0x40)
+ local lowpart = codepoint - (0x40 * highpart)
+ return string.char(0xC0 + highpart,
+ 0x80 + lowpart)
+
+ elseif codepoint <= 65535 then
+ --
+ -- 1110yyyy 10yyyyxx 10xxxxxx
+ --
+ local highpart = math.floor(codepoint / 0x1000)
+ local remainder = codepoint - 0x1000 * highpart
+ local midpart = math.floor(remainder / 0x40)
+ local lowpart = remainder - 0x40 * midpart
+
+ highpart = 0xE0 + highpart
+ midpart = 0x80 + midpart
+ lowpart = 0x80 + lowpart
+
+ --
+ -- Check for an invalid character (thanks Andy R. at Adobe).
+ -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070
+ --
+ if ( highpart == 0xE0 and midpart < 0xA0 ) or
+ ( highpart == 0xED and midpart > 0x9F ) or
+ ( highpart == 0xF0 and midpart < 0x90 ) or
+ ( highpart == 0xF4 and midpart > 0x8F )
+ then
+ return "?"
+ else
+ return string.char(highpart,
+ midpart,
+ lowpart)
+ end
+
+ else
+ --
+ -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
+ --
+ local highpart = math.floor(codepoint / 0x40000)
+ local remainder = codepoint - 0x40000 * highpart
+ local midA = math.floor(remainder / 0x1000)
+ remainder = remainder - 0x1000 * midA
+ local midB = math.floor(remainder / 0x40)
+ local lowpart = remainder - 0x40 * midB
+
+ return string.char(0xF0 + highpart,
+ 0x80 + midA,
+ 0x80 + midB,
+ 0x80 + lowpart)
+ end
+end
+
+function OBJDEF:onDecodeError(message, text, location, etc)
+ if text then
+ if location then
+ message = string.format("%s at char %d of: %s", message, location, text)
+ else
+ message = string.format("%s: %s", message, text)
+ end
+ end
+ if etc ~= nil then
+ message = message .. " (" .. OBJDEF:encode(etc) .. ")"
+ end
+
+ if self.assert then
+ self.assert(false, message)
+ else
+ assert(false, message)
+ end
+end
+
+OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError
+OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError
+
+function OBJDEF:onEncodeError(message, etc)
+ if etc ~= nil then
+ message = message .. " (" .. OBJDEF:encode(etc) .. ")"
+ end
+
+ if self.assert then
+ self.assert(false, message)
+ else
+ assert(false, message)
+ end
+end
+
+local function grok_number(self, text, start, etc)
+ --
+ -- Grab the integer part
+ --
+ local integer_part = text:match('^-?[1-9]%d*', start)
+ or text:match("^-?0", start)
+
+ if not integer_part then
+ self:onDecodeError("expected number", text, start, etc)
+ end
+
+ local i = start + integer_part:len()
+
+ --
+ -- Grab an optional decimal part
+ --
+ local decimal_part = text:match('^%.%d+', i) or ""
+
+ i = i + decimal_part:len()
+
+ --
+ -- Grab an optional exponential part
+ --
+ local exponent_part = text:match('^[eE][-+]?%d+', i) or ""
+
+ i = i + exponent_part:len()
+
+ local full_number_text = integer_part .. decimal_part .. exponent_part
+ local as_number = tonumber(full_number_text)
+
+ if not as_number then
+ self:onDecodeError("bad number", text, start, etc)
+ end
+
+ return as_number, i
+end
+
+
+local function grok_string(self, text, start, etc)
+
+ if text:sub(start,start) ~= '"' then
+ self:onDecodeError("expected string's opening quote", text, start, etc)
+ end
+
+ local i = start + 1 -- +1 to bypass the initial quote
+ local text_len = text:len()
+ local VALUE = ""
+ while i <= text_len do
+ local c = text:sub(i,i)
+ if c == '"' then
+ return VALUE, i + 1
+ end
+ if c ~= '\\' then
+ VALUE = VALUE .. c
+ i = i + 1
+ elseif text:match('^\\b', i) then
+ VALUE = VALUE .. "\b"
+ i = i + 2
+ elseif text:match('^\\f', i) then
+ VALUE = VALUE .. "\f"
+ i = i + 2
+ elseif text:match('^\\n', i) then
+ VALUE = VALUE .. "\n"
+ i = i + 2
+ elseif text:match('^\\r', i) then
+ VALUE = VALUE .. "\r"
+ i = i + 2
+ elseif text:match('^\\t', i) then
+ VALUE = VALUE .. "\t"
+ i = i + 2
+ else
+ local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
+ if hex then
+ i = i + 6 -- bypass what we just read
+
+ -- We have a Unicode codepoint. It could be standalone, or if in the proper range and
+ -- followed by another in a specific range, it'll be a two-code surrogate pair.
+ local codepoint = tonumber(hex, 16)
+ if codepoint >= 0xD800 and codepoint <= 0xDBFF then
+ -- it's a hi surrogate... see whether we have a following low
+ local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
+ if lo_surrogate then
+ i = i + 6 -- bypass the low surrogate we just read
+ codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16)
+ else
+ -- not a proper low, so we'll just leave the first codepoint as is and spit it out.
+ end
+ end
+ VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)
+
+ else
+
+ -- just pass through what's escaped
+ VALUE = VALUE .. text:match('^\\(.)', i)
+ i = i + 2
+ end
+ end
+ end
+
+ self:onDecodeError("unclosed string", text, start, etc)
+end
+
+local function skip_whitespace(text, start)
+
+ local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2
+ if match_end then
+ return match_end + 1
+ else
+ return start
+ end
+end
+
+local grok_one -- assigned later
+
+local function grok_object(self, text, start, etc)
+ if not text:sub(start,start) == '{' then
+ self:onDecodeError("expected '{'", text, start, etc)
+ end
+
+ local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'
+
+ local VALUE = self.strictTypes and self:newObject { } or { }
+
+ if text:sub(i,i) == '}' then
+ return VALUE, i + 1
+ end
+ local text_len = text:len()
+ while i <= text_len do
+ local key, new_i = grok_string(self, text, i, etc)
+
+ i = skip_whitespace(text, new_i)
+
+ if text:sub(i, i) ~= ':' then
+ self:onDecodeError("expected colon", text, i, etc)
+ end
+
+ i = skip_whitespace(text, i + 1)
+
+ local val, new_i = grok_one(self, text, i)
+
+ VALUE[key] = val
+
+ --
+ -- Expect now either '}' to end things, or a ',' to allow us to continue.
+ --
+ i = skip_whitespace(text, new_i)
+
+ local c = text:sub(i,i)
+
+ if c == '}' then
+ return VALUE, i + 1
+ end
+
+ if text:sub(i, i) ~= ',' then
+ self:onDecodeError("expected comma or '}'", text, i, etc)
+ end
+
+ i = skip_whitespace(text, i + 1)
+ end
+
+ self:onDecodeError("unclosed '{'", text, start, etc)
+end
+
+local function grok_array(self, text, start, etc)
+ if not text:sub(start,start) == '[' then
+ self:onDecodeError("expected '['", text, start, etc)
+ end
+
+ local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
+ local VALUE = self.strictTypes and self:newArray { } or { }
+ if text:sub(i,i) == ']' then
+ return VALUE, i + 1
+ end
+
+ local text_len = text:len()
+ while i <= text_len do
+ local val, new_i = grok_one(self, text, i)
+
+ table.insert(VALUE, val)
+
+ i = skip_whitespace(text, new_i)
+
+ --
+ -- Expect now either ']' to end things, or a ',' to allow us to continue.
+ --
+ local c = text:sub(i,i)
+ if c == ']' then
+ return VALUE, i + 1
+ end
+ if text:sub(i, i) ~= ',' then
+ self:onDecodeError("expected comma or '['", text, i, etc)
+ end
+ i = skip_whitespace(text, i + 1)
+ end
+ self:onDecodeError("unclosed '['", text, start, etc)
+end
+
+
+grok_one = function(self, text, start, etc)
+ -- Skip any whitespace
+ start = skip_whitespace(text, start)
+
+ if start > text:len() then
+ self:onDecodeError("unexpected end of string", text, nil, etc)
+ end
+
+ if text:find('^"', start) then
+ return grok_string(self, text, start, etc)
+
+ elseif text:find('^[-0123456789 ]', start) then
+ return grok_number(self, text, start, etc)
+
+ elseif text:find('^%{', start) then
+ return grok_object(self, text, start, etc)
+
+ elseif text:find('^%[', start) then
+ return grok_array(self, text, start, etc)
+
+ elseif text:find('^true', start) then
+ return true, start + 4
+
+ elseif text:find('^false', start) then
+ return false, start + 5
+
+ elseif text:find('^null', start) then
+ return nil, start + 4
+
+ else
+ self:onDecodeError("can't parse JSON", text, start, etc)
+ end
+end
+
+function OBJDEF:decode(text, etc)
+ if type(self) ~= 'table' or self.__index ~= OBJDEF then
+ OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc)
+ end
+
+ if text == nil then
+ self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc)
+ elseif type(text) ~= 'string' then
+ self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc)
+ end
+
+ if text:match('^%s*$') then
+ return nil
+ end
+
+ if text:match('^%s*<') then
+ -- Can't be JSON... we'll assume it's HTML
+ self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc)
+ end
+
+ --
+ -- Ensure that it's not UTF-32 or UTF-16.
+ -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),
+ -- but this package can't handle them.
+ --
+ if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then
+ self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc)
+ end
+
+ local success, value = pcall(grok_one, self, text, 1, etc)
+ if success then
+ return value
+ else
+ -- should never get here... JSON parse errors should have been caught earlier
+ assert(false, value)
+ return nil
+ end
+end
+
+local function backslash_replacement_function(c)
+ if c == "\n" then
+ return "\\n"
+ elseif c == "\r" then
+ return "\\r"
+ elseif c == "\t" then
+ return "\\t"
+ elseif c == "\b" then
+ return "\\b"
+ elseif c == "\f" then
+ return "\\f"
+ elseif c == '"' then
+ return '\\"'
+ elseif c == '\\' then
+ return '\\\\'
+ else
+ return string.format("\\u%04x", c:byte())
+ end
+end
+
+local chars_to_be_escaped_in_JSON_string
+ = '['
+ .. '"' -- class sub-pattern to match a double quote
+ .. '%\\' -- class sub-pattern to match a backslash
+ .. '%z' -- class sub-pattern to match a null
+ .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters
+ .. ']'
+
+local function json_string_literal(value)
+ local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function)
+ return '"' .. newval .. '"'
+end
+
+local function object_or_array(self, T, etc)
+ --
+ -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON
+ -- object. If there are only numbers, it's a JSON array.
+ --
+ -- If we'll be converting to a JSON object, we'll want to sort the keys so that the
+ -- end result is deterministic.
+ --
+ local string_keys = { }
+ local seen_number_key = false
+ local maximum_number_key
+
+ for key in pairs(T) do
+ if type(key) == 'number' then
+ seen_number_key = true
+ if not maximum_number_key or maximum_number_key < key then
+ maximum_number_key = key
+ end
+ elseif type(key) == 'string' then
+ table.insert(string_keys, key)
+ else
+ self:onEncodeError("can't encode table with a key of type " .. type(key), etc)
+ end
+ end
+
+ if seen_number_key and #string_keys > 0 then
+ --
+ -- Mixed key types... don't know what to do, so bail
+ --
+ self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc)
+
+ elseif #string_keys == 0 then
+ --
+ -- An array
+ --
+ if seen_number_key then
+ return nil, maximum_number_key -- an array
+ else
+ --
+ -- An empty table...
+ --
+ if tostring(T) == "JSON array" then
+ return nil
+ elseif tostring(T) == "JSON object" then
+ return { }
+ else
+ -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects
+ return nil
+ end
+ end
+ else
+ --
+ -- An object, so return a list of keys
+ --
+ table.sort(string_keys)
+ return string_keys
+ end
+end
+
+--
+-- Encode
+--
+local encode_value -- must predeclare because it calls itself
+function encode_value(self, value, parents, etc)
+
+
+ if value == nil then
+ return 'null'
+ end
+
+ if type(value) == 'string' then
+ return json_string_literal(value)
+ elseif type(value) == 'number' then
+ if value ~= value then
+ --
+ -- NaN (Not a Number).
+ -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option.
+ --
+ return "null"
+ elseif value >= math.huge then
+ --
+ -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should
+ -- really be a package option. Note: at least with some implementations, positive infinity
+ -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is.
+ -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">="
+ -- case first.
+ --
+ return "1e+9999"
+ elseif value <= -math.huge then
+ --
+ -- Negative infinity.
+ -- JSON has no INF, so we have to fudge the best we can. This should really be a package option.
+ --
+ return "-1e+9999"
+ else
+ return tostring(value)
+ end
+ elseif type(value) == 'boolean' then
+ return tostring(value)
+
+ elseif type(value) ~= 'table' then
+ self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)
+
+ else
+ --
+ -- A table to be converted to either a JSON object or array.
+ --
+ local T = value
+
+ if parents[T] then
+ self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
+ else
+ parents[T] = true
+ end
+
+ local result_value
+
+ local object_keys, maximum_number_key = object_or_array(self, T, etc)
+ if maximum_number_key then
+ --
+ -- An array...
+ --
+ local ITEMS = { }
+ for i = 1, maximum_number_key do
+ table.insert(ITEMS, encode_value(self, T[i], parents, etc))
+ end
+
+ result_value = "[" .. table.concat(ITEMS, ",") .. "]"
+ elseif object_keys then
+ --
+ -- An object
+ --
+
+ --
+ -- We'll always sort the keys, so that comparisons can be made on
+ -- the results, etc. The actual order is not particularly
+ -- important (e.g. it doesn't matter what character set we sort
+ -- as); it's only important that it be deterministic... the same
+ -- every time.
+ --
+ local PARTS = { }
+ for _, key in ipairs(object_keys) do
+ local encoded_key = encode_value(self, tostring(key), parents, etc)
+ local encoded_val = encode_value(self, T[key], parents, etc)
+ table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
+ end
+ result_value = "{" .. table.concat(PARTS, ",") .. "}"
+ else
+ --
+ -- An empty array/object... we'll treat it as an array, though it should really be an option
+ --
+ result_value = "[]"
+ end
+
+ parents[T] = false
+ return result_value
+ end
+end
+
+local encode_pretty_value -- must predeclare because it calls itself
+function encode_pretty_value(self, value, parents, indent, etc)
+
+ if type(value) == 'string' then
+ return json_string_literal(value)
+
+ elseif type(value) == 'number' then
+ return tostring(value)
+
+ elseif type(value) == 'boolean' then
+ return tostring(value)
+
+ elseif type(value) == 'nil' then
+ return 'null'
+
+ elseif type(value) ~= 'table' then
+ self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)
+
+ else
+ --
+ -- A table to be converted to either a JSON object or array.
+ --
+ local T = value
+
+ if parents[T] then
+ self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
+ end
+ parents[T] = true
+
+ local result_value
+
+ local object_keys = object_or_array(self, T, etc)
+ if not object_keys then
+ --
+ -- An array...
+ --
+ local ITEMS = { }
+ for i = 1, #T do
+ table.insert(ITEMS, encode_pretty_value(self, T[i], parents, indent, etc))
+ end
+
+ result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
+
+ else
+
+ --
+ -- An object -- can keys be numbers?
+ --
+
+ local KEYS = { }
+ local max_key_length = 0
+ for _, key in ipairs(object_keys) do
+ local encoded = encode_pretty_value(self, tostring(key), parents, "", etc)
+ max_key_length = math.max(max_key_length, #encoded)
+ table.insert(KEYS, encoded)
+ end
+ local key_indent = indent .. " "
+ local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)
+ local FORMAT = "%s%" .. tostring(max_key_length) .. "s: %s"
+
+ local COMBINED_PARTS = { }
+ for i, key in ipairs(object_keys) do
+ local encoded_val = encode_pretty_value(self, T[key], parents, subtable_indent, etc)
+ table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
+ end
+ result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
+ end
+
+ parents[T] = false
+ return result_value
+ end
+end
+
+function OBJDEF:encode(value, etc)
+ if type(self) ~= 'table' or self.__index ~= OBJDEF then
+ OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
+ end
+
+ local parents = {}
+ return encode_value(self, value, parents, etc)
+end
+
+function OBJDEF:encode_pretty(value, etc)
+ local parents = {}
+ local subtable_indent = ""
+ return encode_pretty_value(self, value, parents, subtable_indent, etc)
+end
+
+function OBJDEF.__tostring()
+ return "JSON encode/decode package"
+end
+
+OBJDEF.__index = OBJDEF
+
+function OBJDEF:new(args)
+ local new = { }
+
+ if args then
+ for key, val in pairs(args) do
+ new[key] = val
+ end
+ end
+
+ return setmetatable(new, OBJDEF)
+end
+
+return OBJDEF:new()
+
+--
+-- Version history:
+--
+-- 20130120.6 Comment update: added a link to the specific page on my blog where this code can
+-- be found, so that folks who come across the code outside of my blog can find updates
+-- more easily.
+--
+-- 20111207.5 Added support for the 'etc' arguments, for better error reporting.
+--
+-- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent.
+--
+-- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules:
+--
+-- * When encoding lua for JSON, Sparse numeric arrays are now handled by
+-- spitting out full arrays, such that
+-- JSON:encode({"one", "two", [10] = "ten"})
+-- returns
+-- ["one","two",null,null,null,null,null,null,null,"ten"]
+--
+-- In 20100810.2 and earlier, only up to the first non-null value would have been retained.
+--
+-- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999".
+-- Version 20100810.2 and earlier created invalid JSON in both cases.
+--
+-- * Unicode surrogate pairs are now detected when decoding JSON.
+--
+-- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding
+--
+-- 20100731.1 initial public release
+--