--- Get a preview image corresponding to specified photo, at the specified level, if possible.
--
-- @param photo (LrPhoto or table of param, required) specified photo or table of named parameters same as below including photo=lr-photo:
-- @param photoPath (string, optional) photo-path if available, otherwise will be pulled from raw-metadata.
-- @param previewFile (string, default=unique-temp-path) target path to store jpeg - if non-vil value passed and file is pre-existing, it will be overwritten.
-- @param level (number, required) appx sizes + intended use:
-- <br> 1 - 80x60 small thumb
-- <br> 2 - 160x120 medium thumb
-- <br> 3 - 320x240 large thumb
-- <br> 4 - 640x480 small image
-- <br> 5 - 1280x960 medium image
-- <br> 6 - 2560x1920 large image
-- <br> 7 - 1:1 full-res
-- @param minLevel (number, default=1) minimum acceptable level.
--
-- @usage file, errm, level = cat:getPreview{ photo=catalog:getTargetPhoto(), level=5 }
-- @usage file, errm, level = cat:getPreview( catalog:getTargetPhoto(), nil, nil, 5 )
--
-- @return file (string, or nil) path to file containing requested preview (may be the same as preview-file passed in).
-- @return errm (string, or nil) error message if unable to obtain requested preview (includes path(s)).
-- @return level (number, or nil) actual level read, which may be different than requested level if min-level passed in.
--
function Catalog:getPreview( photo, photoPath, previewFile, level, minLevel )
if photo == nil then
app:callingError( "no photo" )
end
if not photo.catalog then -- not lr-photo
photoPath = photo.photoPath
previewFile = photo.previewFile
-- assert( photo.level, "no level in param table" )
level = photo.level
minLevel = photo.minLevel
photo = photo.photo
-- assert( photo and photo.catalog, "no lr-photo in param table" )
end
if level == nil then
app:callingError( "no level" )
end
if level > 7 then
app:logWarning( "Max level is 7" )
level = 7
end
if photoPath == nil then
photoPath = photo:getRawMetadata( 'path' )
end
local photoFilename = LrPathUtils.leafName( photoPath )
local _previewFile
if previewFile == nil then
_previewFile = LrPathUtils.child( LrPathUtils.getStandardFilePath( 'temp' ), str:fmt( "^1.lrPreview.jpg", photoFilename ) ) -- include extension, since there are separate previews for each file-type.
else
if fso:existsAsFile( previewFile ) then
app:logVerbose( "preview path passed is to existing file to be overwritten" )
end
_previewFile = previewFile
end
local imageId
local s = tostring( photo ) -- THIS IS WHAT ALLOWS IT TO WORK DESPITE LOCKED DATABASE (id is output by to-string method).
local p1, p2 = s:find( 'id "' )
if p1 then
s = s:sub( p2 + 1 )
p1, p2 = s:find( '" )' )
if p1 then
imageId = s:sub( 1, p1-1 )
end
end
if imageId == nil then
return nil, "bad id"
end
local cp = catalog:getPath()
local fn = LrPathUtils.leafName( cp )
local n = LrPathUtils.removeExtension( fn )
local cd = LrPathUtils.parent( cp )
local pn = n .. " Previews.lrdata"
local d = LrPathUtils.child( cd, pn )
local pdb = LrPathUtils.child( d, 'previews.db' )
assert( fso:existsAsFile( pdb ), "nope" )
--Debug.pause( pdb )
local exe = app:getPref( 'sqlite3' )
if not str:is( exe ) then
if WIN_ENV then
exe = LrPathUtils.child( _PLUGIN.path, "sqlite3.exe" )
else
exe = LrPathUtils.child( _PLUGIN.path, "sqlite3" )
end
app:logVerbose( "Using sqlite executable included with plugin: ^1", exe )
else
app:logVerbose( "Using custom sqlite executable: ^1", exe )
end
local param = '"' .. pdb .. '"'
local targ = str:fmt( "select uuid, digest from ImageCacheEntry where imageId=^1", imageId )
local r1, r2, r3 = app:executeCommand( exe, param, { targ }, nil, 'del' )
local uuid -- of preview
local digest -- of preview
if r1 then
if r3 then
local c = str:split( r3, '|' )
if #c >= 2 then
-- good
uuid = c[1]
digest = c[2]
else
return nil, "bad split"
end
else
return nil, "no content"
end
else
return nil, r2
end
local previewSubdir = str:getFirstChar( uuid )
local pDir = LrPathUtils.child( d, previewSubdir )
if fso:existsAsDir( pDir ) then
-- good
else
return nil, "preview letter dir does not exist: " .. pDir
end
previewSubdir = uuid:sub( 1, 4 )
pDir = LrPathUtils.child( pDir, previewSubdir )
if fso:existsAsDir( pDir ) then
-- good
else
return nil, "preview 4-some dir does not exist: " .. pDir
end
local previewFilename = uuid .. '-' .. digest .. ".lrprev"
local previewPath = LrPathUtils.child( pDir, previewFilename )
if fso:existsAsFile( previewPath ) then
app:logVerbose( "Found preview file at ^1", previewPath )
else
return nil, str:fmt( "No preview file corresponding to ^1 at ^2", photo:getRawMetadata( 'photoPath' ), previewPath )
end
-- this could be modified to return image data instead of file if need be.
local content
local function getImageFile()
local p1, p2 = content:find( "level_" .. str:to( level ) )
if p1 then
local start = p2 + 2 -- jump over level_n\0
local p3 = content:find( "AgHg", start )
local stop
if p3 then
stop = start + p3 - 1
else
stop = content:len() - 1
end
local data = content:sub( start, stop )
if previewFile ~= nil then -- user passed file
app:logVerbose( "Writing preview into user file: ^1", _previewFile )
else
-- rename file to include level.
local base = LrPathUtils.removeExtension( _previewFile ) .. '_' .. level
_previewFile = base .. ".jpg"
app:logVerbose( "Writing preview into default-named file: ^1", _previewFile )
end
local s, m = fso:writeFile( _previewFile, data )
if s then
app:logVerbose( "Wrote preview file: ^1", _previewFile )
return _previewFile
else
return nil, m
end
else
return nil -- no real error, just no preview at that level.
end
end
minLevel = minLevel or 1
local status
status, content = LrTasks.pcall( LrFileUtils.readFile, previewPath )
if status and content then
repeat
local file, errm = getImageFile() -- at level
if file then
return file, nil, level
elseif errm then
return nil, errm
elseif level > minLevel then
level = level - 1
else
return nil, str:fmt( "No preview for ^1 at any acceptable level", photoPath )
end
until level <= 0
return nil, str:fmt( "Unable to obtain preview for ^1", photoPath )
else
return nil, str:fmt( "Unable to read preview source file at ^1, error message: ^2", previewPath, content )
end
end
This function is working great so far, but as of 2011-09-29 it has not been rigorously tested, so it may have a bug or two...
It is based on the elare plugin framework available here (including source code): https://www.assembla.com/spaces/lrdevplugin/
You will need sqlite3 executable from here: http://www.sqlite.org/sqlite.html
- put it in lr(dev)plugin dir
Note: view-factory's picture component will accept a path as resource id, so to use:
local pictureFile, errm = cat:getPreview( photo, nil, nil, 4 )
if pictureFile then
items[#items + 1] = vf:picture {
value = pictureFile,
}
end
Note: the above code is intended for "sample/example" only -
MUST DO:
- Handle portrait orientation properly...
MAYBE DO:
- Handle AdobeRGB profile assignment - not needed for thumbs, but maybe for the biggies...
- Optimize for multi-photo use.
- Change detection for sake of caching for repeated access (like scrolling thumbnails).
@2011-10-04, the code at Assembla.com (see link above) takes care of all these things, and then some...;-)
Rob