Copy link to clipboard
Copied
Hey folks, seeing as I hang here most, I figure it's the best place for my 4000th post on Adobe Forums (get a life!).
Does anyone have a little tutorial on passing the rendered photos to an external file? I want it inside a normal plugin, not as a post process action.
It's for an entirely personal project, passing images to ffmpeg to have my timelapse movies created with an export from Lightroom.
In your processRenderedPhotos(), you have a loop where you go through all of the exportContext:renditions one by one (standard stuff for almost all export plugins). You're probably launching the app within that loop. Move it after the loop so it launches a single time once all photos are rendered.
Copy link to clipboard
Copied
Sean - Are you looking for sample code that sets up a command line and then launches an external app? I have this for ImageMagick's convert which I can post here along with comments. My code does this once per exported image though rather than build a list of the images and pass them all at once which is what it sounds like you're trying to do. Not that adding that extra bit should be hard. Let me know and I'll share if relevant.
Dave
Copy link to clipboard
Copied
Sounds close Dave..
I'd need to pass 3 variables: frame rate, data rate and output filename.
I assume it'll be of the type local status = LrTasks.execute( 'mytool "' .. pathOrMessage .. '"' )
But I'm not sure how to add multiple variables. I'm not sure what the correct mix of '"..xxx.."' is!
A sample call would be ffmpeg -r 10 -b 1800 -i %03d.jpg test1800.mp4
-r=frame rate -b=bit rate -i image name as a sequence(which is done with the renditions) then output.
These variables will be handled by prefs (because once I'm happy with the settings, I'll probably stick with them)
UploadTask.exportPresetFields = {
{ key = 'fr', default = '24' },
{ key = 'dr', default = '1800' },
{ key = 'moviename', default = '/movie.mp4' },
{ key = 'LR_export_destinationType', default = 'tempFolder' },
}
Copy link to clipboard
Copied
Does this snippet from my exiftool plugin help? It's sending mulitple variables to the selected thumbnail (effectively extending Ctrl/Cmd S) .
John
if WIN_ENV == true then command = '"' .. LrPathUtils.child(LrPathUtils.child( _PLUGIN.path, "win"), "exiftool.exe") .. '" ' .. newUrgency .. ' "' .. phoPath .. '" ' .. newOverwrite quotedCommand = '"' .. command .. '"'
else command = '"' .. LrPathUtils.child(LrPathUtils.child(_PLUGIN.path, "mac"), "exiftool") .. '" ' .. newUrgency .. ' "' .. phoPath .. '" ' .. newOverwrite quotedCommand = command
end
Copy link to clipboard
Copied
John's reply shows what to do in essential form. With no offense intended to John, I think this can be improved upon in two ways. I find that all of the concatenation operators make things hard to read. I also find that keeping entirely separate versions for Windows vs. Mac is potentially error prone when the only thing that actually changes is the name of the executable.
What I prefer to do is make a table for the parameters, insert them one by one, then concatenate the table contents at the end. Here is an abridged version of the code I use with edits for your app (this formatting stinks - John, how did you get your code to look like that in the embedded window?):
local params = {}
if WIN_ENV == true then
table.insert( params, '"ffmpeg.exe"')
else
table.insert( params, '"ffmpeg"')
end
table.insert( params, "-r " .. exportPresetFields.fr )
table.insert( params, "-b " .. exportPresetFields.br )
table.insert( params, "-i " .. exportPresetFields.moviename )
-- combine params into one string with a space inbetween
local execString = table.concat( params, " " )
logmsg ( "Running app with shell command: " .. execString )
local result = LrTasks.execute( execString )
logmsg( "The result is: " .. result );
if ( result ~= 0 ) then
logmsg( "ERROR: " .. result )
end
The logmsg() function is just my wrapper around a LR logging function. This is all a bit simplified from my actual code for clarity. For instance, to collect the output of the application into a file, I actually have this as the very last thing inserted into the table:
if ( debugMode ) then
table.insert( params, ">> debugLog.txt 2>&1" )
end
This redirects the normal and error output into debugLog.txt. If I recall, you're on a Mac. I'm on Windows and I can offer up another trick or two about managing the console window that appears (or not), etc. but maybe not important since this is a plugin for yourself.
Copy link to clipboard
Copied
No offence taken, Dave. I'm now picking up speed with this Lua stuff and am very confident I'm doing some things in a crude and inelegant manner!
To get the code looking OK here, I switched to HTML view and then use the pre HTML tag.
John
Copy link to clipboard
Copied
There's even more answer to questions I hadn't asked Dave, cheers.
e.g. I've been sending logs to the Console already.. which is how I was able to figure out that TweetPhoto was having server hiccups. I wasn't sure how to fit text in with the log message though.
Copy link to clipboard
Copied
Sean - I have no access to a Mac so please let me know if the code works as expected there. Especially the redirect to a file. It comes down to how Adobe has chosen to launch other apps from LR on the Mac. If they're launching it via the default command shell (which I believe is bash on a Mac) then that redirect should work. This is useful for me as well since when it comes time to make a plugin available to the community, it's one less reason for me to go buy a Mac just to test it. 🙂
Copy link to clipboard
Copied
Will do.
I may just try this as a post process action after all.
Copy link to clipboard
Copied
FYI The reason for that Mac/Win code was originally because of these shell differences. On PC, I could use LrShell to launch a PDF directly - just sending its name launched it in Acrobat Reader. That default app approach failed on Mac and I was forced to point LrTasks at Preview.
John
Copy link to clipboard
Copied
I've got to the point where I'm feeding the params in, but.....
It's processing on a per photo basis. What's the trick to force it wait until all photos have rendered before executing?
Copy link to clipboard
Copied
In your processRenderedPhotos(), you have a loop where you go through all of the exportContext:renditions one by one (standard stuff for almost all export plugins). You're probably launching the app within that loop. Move it after the loop so it launches a single time once all photos are rendered.
Copy link to clipboard
Copied
DFBurns wrote:
...local params = {}
if WIN_ENV == true then
table.insert( params, '"ffmpeg.exe"')
else
table.insert( params, '"ffmpeg"')
endtable.insert( params, "-r " .. exportPresetFields.fr )
...
Thanks for sharing! This approach is much more elegant than what I had been using up until now.
Matt
Copy link to clipboard
Copied
Almost there.. It's all passing correctly now.. Thanks again Dave.
Changes I had to make: filterContext.propertyTable.fr for exportPresetFields.fr, etc
I added in ffmpegPath=LrPathUtils.parent(pathOrMessage) to allow me to tell ffmpeg where the files were, as it requires a sequence as input.
I also used that path as a prefix for the move location output folder.
Now I just need to get an acceptable quality movie as output, because currently the quality SUCKS!
Copy link to clipboard
Copied
That's probably a matter of choosing the right options for ffmpeg. I've never used it myself but I've heard that it's pretty decent. When I've done time-lapse movies, I did it manually using Quicktime Pro. A quick Google around shows that there is a scripting interface for QT at least on Windows using COM. You can write that script (sort of like VBScript) to a file then launch that script to do the work. A kludge but it'd probably work. 🙂 See here for info:
http://www.xsi-blog.com/archives/103
Copy link to clipboard
Copied
Hi Dave,
My post above was being made as you posted.. so we crossed over.
The point was that I didn't actually want to have to have another program open to run this.. And of course I'm on a mac. I did look into Applescript and Automator, but couldn't find how to call Open Image Sequence from them.. Anyhow all working.
I just need to add an Overwrite Movie checkbox and maybe change the frame rate and data rate to popup menus... and I'm done.
Again this works for me because I have ffmpeg compiled and living in my /usr/bin folder..
I suspect I'd need to make it ./ffmpeg to run locally inside the plugin (haven't tried)..
And so you get the 10 points too
Copy link to clipboard
Copied
Good advice to build your argument string using a table and then concat the table at the end - as well as tidier code, this is faster than building strings using 'x .. y'. You can make that faster still by using:
myTable[#myTable + 1] = "some text"
rather than:
table.insert(myTable, "some text")
An inline expression is generally faster than calling a function.
Sean, on your question of finding the last photo from filterContext:renditions - if you know all the files are the same size, can't you get your values once and not call the two functions again - i.e. get the first photo's data rather than try to get the last.
Copy link to clipboard
Copied
True, and I was thinking this myself today.
Copy link to clipboard
Copied
Joy.
It works...
The movie sucked because 2 images were at 1080X720, with the rest at 1280x720 causing a bit of shoehorning. All fine now...
Here's the (probably sucky) code: (please note the comment in the middle, if anyone knows the answer to that, please pipe up!)
function ffmpegFilterProvider.postProcessRenderedPhotos( functionContext, filterContext ) local renditionOptions = { plugin = _PLUGIN, renditionsToSatisfy = filterContext.renditionsToSatisfy, filterSettings = function( renditionToSatisfy, exportSettings ) -- This hook function gives you the opportunity to change the render -- settings for each photo before Lightroom renders it. end, } for sourceRendition, renditionToSatisfy in filterContext:renditions(renditionOptions) do -- Wait for the upstream task to finish its work on this photo. local success, pathOrMessage = sourceRendition:waitForRender() if success then
-- take the file path and image size
-- because this is for me, I know the files have to have the same crop, so taking it from the final
-- rendered file is okay. If someone wants to suggest how we know the file is the final one to render, then
-- I could simply call that one, rather than calling LrPathUtils and LrPhotoInfo each render
ffmpegPath=LrPathUtils.parent(pathOrMessage) movieSize=LrPhotoInfo.fileAttributes(pathOrMessage) end end local params = {} local movie={} table.insert( movie, ffmpegPath) table.insert( movie, "/") table.insert( movie, filterContext.propertyTable.moviename) local movieString = table.concat( movie, "" ) if WIN_ENV == true then table.insert( params, '"ffmpeg.exe"') else table.insert( params, '"ffmpeg"') end table.insert( params, "-r " .. filterContext.propertyTable.fr ) --Frame Rate table.insert( params, "-b " .. filterContext.propertyTable.dr ) --Bit Rate table.insert( params, "-i " ..ffmpegPath.."/%03d.jpg" ) --Type of Sequence, in this case in the form 001 table.insert( params, "-s " ..movieSize.width.."x"..movieSize.height ) --Movie Size table.insert( params, "" .. movieString ) --Movie path and filename -- combine params into one string with a space inbetween local execString = table.concat( params, " " ) log:trace(execString) local result = LrTasks.execute( execString ) log:trace( "The result is: " .. result ); if ( result ~= 0 ) then log:trace( "ERROR: " .. result ) end end
Copy link to clipboard
Copied
Matt - It really keeps things sane when you have 18 command line options to pass to ImageMagick like my code does. When/if you need to reorder options is cleaner with this technique as well. Just cut the line and paste it in the new location. Cheers,
Dave
Find more inspiration, events, and resources on the new Adobe Community
Explore Now