Skip navigation
Currently Being Moderated

Here is a function to read preview images from active catalog

Oct 4, 2011 8:08 AM

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

 
Replies
  • Currently Being Moderated
    Oct 5, 2011 9:21 AM   in reply to Rob Cole

    There's also undocumented view control:

     

    LrPhotoPictureView.makePhotoPictureView({

              width=400,

              height=400,

              photo=catalog:getTargetPhoto()

    })

     

    Should give you a nice view of the currently selected photo if you add this to your view

     

    Jarno

     
    |
    Mark as:
  • Currently Being Moderated
    Oct 5, 2011 9:43 AM   in reply to jarnoh

    Excellent.  How did you learn about this?  (It's good to learn how to fish, rather than be just given fish.)

     
    |
    Mark as:
  • Currently Being Moderated
    Oct 5, 2011 10:09 AM   in reply to John R. Ellis

    In Mac version there's a file called Contents/PlugIns/MultipleMonitor.lrmodule/Contents/Resources/LrPhotoP ictureView.lua

     

    Simple strings command on this gives a lot of clues and the rest is just trying it out.

     

    Jarno

     
    |
    Mark as:
  • Currently Being Moderated
    Oct 5, 2011 10:24 AM   in reply to jarnoh

    Excellent tip, thanks.

     
    |
    Mark as:
  • Currently Being Moderated
    Oct 5, 2011 11:25 AM   in reply to John R. Ellis

    Using the technique of grepping for suspect strings, I found the following additional importable modules. LrRemoteCommunication might be of particular interest to Rob, if he can figure out how to use it:

     

    LrPhotoPictureView

    {--table: 1

        makePhotoPictureView = function: 0000000015836630}

     

    LrRemoteCommunication

    {--table: 1

        closeNamedConnection = function: 00000000114B3570,

        pollForMessage = function: 00000000159251F0,

        spawnTaskAndConnect = function: 0000000013EF8610,

        sendMessageToServer = function: 00000000114B3660,

        launchApplicationWithPath = function: 0000000015A592D0}

     

    LrTableUtils

    {--table: 1

        debugDumpTable = function: 0000000000589430}

     

    LrUUID
    {--table: 1
        generateUUID = function: 0000000005B75140}

     
    |
    Mark as:
  • Currently Being Moderated
    Oct 7, 2011 10:15 AM   in reply to jarnoh

    jarnoh wrote:

     

    There's also undocumented view control:

     

    LrPhotoPictureView.makePhotoPictureView({

              width=400,

              height=400,

              photo=catalog:getTargetPhoto()

    })

     

    Should give you a nice view of the currently selected photo if you add this to your view

     

    Cool. Alas, it appears that width, height cannot be bound to an observable table. I suppose this sort of makes sense, as you wouldn't want the photo jumping around in the container.

     

    But as a trick for making invisible objects not take up any space, setting this to 0 when a view is first brought up would be nice.

     

    Duh. I should learn how to use  LrView.conditionalItem() shouldn't I?

     

    Message was edited by: clvrmnky

     
    |
    Mark as:
  • Currently Being Moderated
    Jan 14, 2012 11:57 AM   in reply to jarnoh

    I did some digging, and it looks like LR4 beta has new view component, viewFactory:catalog_photo.  API seems to work just like LrPhotoPictureView.

     
    |
    Mark as:
  • Currently Being Moderated
    Jan 14, 2012 1:50 PM   in reply to jarnoh

    Neat, thanks.

     
    |
    Mark as:

More Like This

  • Retrieving data ...

Bookmarked By (0)

Answers + Points = Status

  • 10 points awarded for Correct Answers
  • 5 points awarded for Helpful Answers
  • 10,000+ points
  • 1,001-10,000 points
  • 501-1,000 points
  • 5-500 points