Skip to main content

⚠️ Please note that this topic or post has been archived. The information contained here may no longer be accurate or up-to-date. ⚠️

Moving selecting images into new folder and add it

Comments

8 comments

  • Eric Valk
    I'm working on a utility to do that kind of thing, almost finished.

    Can you share the following information:
    What is the min, typical and max time interval between images in one Panorama set? How many images in a Pano set?

    Would a script recognise the differences between images in one Panaorama set and the next (or images not in a Panaorama set) by the time interval or by some other way? If by time interval, what do you think a good threshold would be?

    Is there other EXIF data in the files that could be used to separate one sequence from another? If so, what kind of camera body are you using?

    Do you shoot RAW and JPEG, or RAW only, or ??

    My script organises images into a sequence, by time interval, and by parsing the file names. It then assigns a name to the sequence. The information is stored in IPTC metadata fields of the variants. I will "hijack" one of the two following sets of IPTC fields:

    Option 1:
    -- Sequence ID: <--> "contact city" {Base name of first Image in the sequence - i.e. not including the extension}
    -- Sequence Count: <--> "contact state" {1,2,3,4 ....}
    -- Sequence Total: <--> "contact postal code"
    -- Sequence Type: <--> "contact country" {Bracket, Burst, Pano}

    Option 2:
    -- Sequence ID: <--> "image city" {Base name of first Image in the sequence - i.e. not including the extension}
    -- Sequence Count: <--> "image state" {1,2,3,4 ....}
    -- Sequence Type: <--> "image country" {Bracket, Burst, Pano}
    -- Sequence Total: <--> "image country code"

    Once the Sequnce ID is in the Metadata of every variant, then its not really necessary to create albums for each sequence, since Filter tool will easily select the images by Sequence ID.

    Or is it so? I have some handlers already that create projects and albums and move images into them, could easily be adapted.
    0
  • FL
    Hi Eric,

    sounds interesting:

    What is the min, typical and max time interval between images in one Panorama set? How many images in a Pano set?


    I normally shoot single row. Time between images is typ. "exposure time + 5s". Exposure time is normally less than 1s but sometimes goes up to 10s. Typically I have 12 images in one row. If I do multiple rows the time for changes rows and doing the next shot is somewhat like 10s.

    Would a script recognise the differences between images in one Panaorama set and the next (or images not in a Panaorama set) by the time interval or by some other way? If by time interval, what do you think a good threshold would be?


    I put a (nearly) black images in between Panos. Otherwise I would expect to have at least half a minute to a minute between two different panos. So for me 30 seconds would be a good threshold.

    Is there other EXIF data in the files that could be used to separate one sequence from another? If so, what kind of camera body are you using?


    Nikon. Unfortunately there is no easy way of separating sequences.

    Do you shoot RAW and JPEG, or RAW only, or ??

    RAW only

    My script organises images into a sequence, by time interval, and by parsing the file names. It then assigns a name to the sequence. The information is stored in IPTC metadata fields of the variants. I will "hijack" one of the two following sets of IPTC fields:

    This would work. However I would still prefer moving the files into folders on the hard disk as I use this new folder as a "base folder" for the pano with subfolders for JPEGs and TIFFs respectively. The pano-file (ptgui or Autopano) goes into the base folder and the pano itself into the folder above where all the images from that shooting are kept. In doing so I can easily safe space (delete no longer needed jpg and especially tiff) or delete the complete pano.

    Or is it so? I have some handlers already that create projects and albums and move images into them, could easily be adapted.

    Would probably a good starting point for me discovering scripting.

    I have another task that might be doable via scripting, i.e. calculating the min(minimum level) and max(maximum level) for the level tool over all images of a pano and then set this for all images.

    Best
    Frank
    0
  • Eric Valk
    Hi Frank
    EDITTED
    Nice to meet someone who is interested in scripting. Your way of thinking seems methodical, and that is necessary fr scripting.

    Then I will share my work, it now has unfinished loose ends that means it is not quite ready for general distribution, but for your level of interest I think it's quite OK.

    At this point, I am just printing a report of how thee images are sorted and grouped. I want to be quite sure that is quite OK before I let the script change Metadata or modify the catalog.

    Note that I have divided the script into functions, these are grouped as follows:

    • Main Part of the script - setup and finish

    • Main Handler - the algorithm itself

    • Script Specific handlers

    • Capture One Handlers

    • Logging handlers

    • Utilities


    In order to understand how this works, I suggest that you start with the main handler and the script specific handlers. Ignore for now Logging handlers and Capture One handlers and the main part of the script.

    One thing to note is that every Applescript Event (Applescript telling an Application like "Capture One" to get data or do something) takes significant time, many of these will slow down the script. So I try to write the script so that it gets as much data as possible from Capture One with one "get" request. For example, this means that instead of asking for data from just one variant, I ask for data from all selected variants or all variants. This is faster if you have more than 5 variants or so.

    There is commented out code that shows how to break up the functions into script library. As a beginner, this is not a good idea. (But you are likely not my only reader). Once you have many scripts that use the same handlers, then Script Libraries start to make a lot of sense. Debugging a handler in a Scrriptt Library is really difficult, so it has to be thoroughly debugged before you put it into a Library Script.

    You are free to use any part of this, if you publish it would appreciate it if you would say that you based your work on mine.

    Do ask questions if you have them.

    I have to break up the script into parts, or it gets too long for one posting. Just copy the parts, one after another, into a Script Editor file.

    The first part
    EDITTED
    I realised that the image name handling is very specific to my file name strategy. Added more generic options.


    (**** Description
    A Script to extract Image Sequence Information for selected Capture One Variants
    The script can handle images with one variant or multiple variants
    The script can handle RAW-JPG image pairs, and even RAW-JPG-TIFF image triplets
    The script finds sequences based on the time interval between images
    This time interval is configurable. For automatic burst and bracketting, 1 second is suitable.
    For user controlled sequencing, depends how fast you work. 20s? 60s? 180s?
    The script has a GUI that allows the user to configure settings
    Version 1217, January 30th , 2019
    !! No Warranty !! Support is best effort
    Eric Valk, Ottawa, Canada

    **** PLanning
    This version only discovers and reports sequences based on intervals.
    The next version will write this into variant Metadata, hijacking some of the IPTC metadata fields
    I also have prototypes that use EXIFtool to get more information about brackts and bursts from the image file.

    *** Usage
    In OSX's Script Editor open a new (blank) script window
    Copy and paste this script into the window
    Save the script, perhaps to your ~/Documents/Scripts folder
    Open the Script Editor log window, and select the messages tab
    In Capture One, Select only a small number of variants, from any collection
    Run the script

    This script does not write or delete any information in the COP Catalog or Session or the image file

    *** Notes
    This script has a GUI that allows the user to configure settings without editing the script
    The user may change the amount of reporting by setting the "debugLogLevel" and "ResultsFileMaxDebug"
    The GUI also allows the user to set the filtering options
    If you find that you have some favorite settings, you can configure them as default at the beginning of the script.
    If you are having some issues, then set debugLogLevel to 4 and send me the results from Script Editors log window, or Text Edit.
    Don't try to change the GUI code unless you are an experienced Apple Script programmer.
    *)
    use AppleScript version "2.5"
    use scripting additions

    ## To use the General Handlers as Script Libraries, comment the lines below
    global U, L, C
    set U to me
    set L to me
    set C to me
    set theMainScript to me

    ## To use the General Handlers as Script Libraries, uncomment the lines below
    ## use U : script "Utilities_1215" -- refers to ~/Library/Scripts/Utilities_1215.sctpd
    ## use L : script "Loqqing_1220" -- refers to ~/Library/Scripts/Loqqing_1220.sctpd
    ## use C : script "CaptureOne_1220" -- refers to ~/Library/Scripts/CaptureOne_1220.sctpd

    ## set L's theMainScript to me
    ## set C's theMainScript to me
    ## if false then true -- change this to trigger a compile of this script. Use when a Library Script file has been uodated

    L's setupLoqqing5() -- setup the variables used for Loqqing system
    ## Values in this section are safe to change, within limits indicated. Support is likely but no commitment

    ## Default values
    set Loqqing's debugLogLevel to 0 -- 0...6 Values >1 result in increasing amounts of debug data that takes longer to report
    set Loqqing's enableResultsFile to true -- (true/false)
    set Loqqing's enableResultsByClipboard to false -- (true/false)
    set Loqqing's enableResultsByDialog to false -- (true/false)
    set Loqqing's maxDialogPercent to 85 -- (0% to 100% of the monitor) The amount of data that triggers a dialog report
    set Loqqing's enableNotifications to false -- (true/false) - enable notifications of errors and exceptions
    set Loqqing's notificationsMaxDebug to 0 -- 0...6 suggest not more than 1
    set Loqqing's ResultsFileMaxDebug to 2 -- 0...6 suggest not more than 2
    set enableFastGui to true -- (true/false)

    set enableIntervalSurvey to true -- reports all images and intervals, for debugging
    set maxSequenceInterval to 500
    set enableSequenceReport to true
    set enableWriteToMetadata to false
    set enableHueristicBracket to true
    set aeBracketStep to 6 -- EV increment for detecting exposure bracketting. 6 enables increments of 1/6 of a stop, good for both ⅓ stop bracketting and ½ stop bracketting
    set enableJpegSequence to true
    set enableRawSequence to true
    set enabledRawFileTypeList to {"RW2", "ORF", "ARW"}
    set enableTiffSequence to true
    set enableImageGrouping to false
    set bnGroup1 to {bnpAlp:false, bnpNum:false, bnpSpa:true, bnpSym:true, bnpBck:false, bnpCnt:4, bnpAct:{}} -- Group 1: Alpha and Numbers OK, stop on Space or symbol or 4 characters
    set bnGroup2 to {bnpAlp:true, bnpNum:false, bnpSpa:true, bnpSym:true, bnpBck:false, bnpCnt:3, bnpAct:{{"$", "R"}}} -- Group 2: Only Numbers OK, stop on Alpha or Space or Symbol or 3 characters. Reset if a "$" is found
    set bnGroup3 to {bnpAlp:true, bnpNum:false, bnpSpa:true, bnpSym:true, bnpBck:true, bnpCnt:5, bnpAct:{{"$", "R"}}} -- Group 2: Only Numbers OK, stop on Alpha or Space or Symbol or 5 characters. Reset if a "$" is found

    #### Edits. Line below is very specific to my file name strategy. Added more generic options
    ## set basicNameParse to setIdBnParsingMethod({{bnGroup1, bnGroup2, bnGroup3}})

    set bnGroup4 to {bnpAlp:false, bnpNum:false, bnpSpa:false, bnpSym:false, bnpBck:false, bnpCnt:12, bnpAct:{}} -- Group 1: take the first 12 characters of any type
    set bnGroup5 to {bnpAlp:false, bnpNum:false, bnpSpa:true, bnpSym:true, bnpBck:true, bnpCnt:5, bnpAct:{}} -- Group 2: take the next 5 numbers or alphabetic characters, stop on a Symbol or Space
    ##set basicNameParse to setIdBnParsingMethod({{bnGroup4, bnGroup5}})

    set bnGroup6 to {bnpAlp:false, bnpNum:false, bnpSpa:true, bnpSym:false, bnpBck:true, bnpCnt:5, bnpAct:{{"-", "G"}}} -- Group 2: take the next 5 characters, stop on a Space or if a "-" is found
    ## set basicNameParse to setIdBnParsingMethod({{bnGroup4, bnGroup6}})

    set bnGroup7 to {bnpAlp:false, bnpNum:false, bnpSpa:false, bnpSym:false, bnpBck:false, bnpCnt:0, bnpAct:{}} -- Group 1: take all characters of any type
    set basicNameParse to setIdBnParsingMethod({{bnGroup7}}) -- this doesn't do any name parsing at all.

    ## Choose lists for GUI
    ## The GUI allows you to change the default by choosing from a choose list
    ## Put your favorites in the default list, other commonly used values in the choose list
    set chooseRawFileExt_L to {"RW2", "ORF", "ARW", "NEF", "CR2"}

    ## ***** Not safe to change stuff below this line, unless you have some background in SW development.
    ## I generally won't help much if you change stuff below this line. I may explain the design intent.

    set debugLogEnable to true

    ## Result reporting methods which are valid for this script
    set Loqqing's gateResultsFile to true
    set Loqqing's gateResultsDialog to true
    set Loqqing's gateResultsByClipboard to true
    set Loqqing's gateResultsDialog to true

    ## Reporting methods which are NOT valid for this script
    -- disable the reporting method
    set Loqqing's gateResultsNotification to false
    set gateResultsInCollection to false
    -- disable user control
    set enableResultsInCollection to false
    set Loqqing's enableResultsByNotifications to false
    ##

    tell application "System Events" to set parent_name to name of current application
    set Script_Title to (get name of me)
    set Result_DocName to "CO_Image_EXIF_Report.txt"
    set Loqqing's enableResultsByLoq to {"Script Editor"} contains parent_name

    set loqResultMethod to L's InitializeLoqqing5(Result_DocName, Script_Title) -- Initialize the results logging system

    set {minCOPversion, maxCOPversion} to {"12", "12"}
    tell C's validateCOP5(minCOPversion, maxCOPversion)
    if its hasErrors then error (get its errorText)
    set {theAppName, copVersion} to {its theAppName, its copVersion}
    end tell

    tell application "Capture One 12" to set COPDocRef to get current document -- not global
    tell C's validateCOPdoc5(COPDocRef, {"Session", "Catalog"})
    if its hasErrors then error (get its errorText)
    set {COPDocName, COPDocKind_s} to {its COPDocName, its COPDocKind_s}
    end tell

    tell C's validateCOPcollections5(COPDocRef) to set {selectedCollectionRef, kindSelectedCollection_s, nameSelectedCollection} to ¬
    {its selectedCollectionRef, its kindSelectedCollection_s, its nameSelectedCollection}

    tell application "Capture One 12" to set countSelectedVariants to get count of selected variants
    tell application "Capture One 12" to set countEveryVariant to count of variants

    if countSelectedVariants = 0 then L's loqqed_Error_Halt5(true, "No images selected")

    L's loq_Results5(-1, false, ("Started from: " & parent_name & " Action: Identify Image Sequences"))


    mainHandler()

    L's loq_Results5(-1, true, (return & "*** " & Script_Title & " is Done ***"))

    ## Arrange the windows to show results on top
    tell application "System Events" to set frontmost of process theAppName to true

    if Loqqing's enableResultsFile then
    tell application "System Events" to set frontmost of process "TextEdit" to true
    else
    tell application "System Events" to set frontmost of process parent_name to true
    end if

    return finalCleanup()

    ###########################################################################################################################
    ## Script Specific Handlers

    on mainHandler()
    global debugLogEnable, basicNameParse, aeBracketStep, maxSequenceInterval, enableIntervalSurvey, enableSequenceReport
    local piName, piId, piExtensionList, piDateList, piVarRefList, varNameList, piIsoList, piApertureList, piShutterList
    set debugLogEnable to false
    tell getAllVariantData() to set {piName, piId, piExtensionList, piDateList, piVarRefList, varNameList, piIsoList, piApertureList, piShutterList} to ¬
    {its piName, its piId, its piExtensionList, its piDateList, its piVarRefList, its varNameList, its piIsoList, its piApertureList, its piShutterList}

    local variantsExtensionList, sortedExtensionsList, extensionCount
    set variantsExtensionList to U's summarizeList(piExtensionList, 0, false)
    set sortedExtensionsList to makeExtensionsList(variantsExtensionList)
    set extensionCount to count of sortedExtensionsList's item 1
    if 0 = extensionCount then error (L's loqqed_Error_Halt5("The selected variants have no file extensions that are enabled. File extensions are: "" & U's joinListToString(variantsExtensionList, "", "") & """))

    local currentImageID, prevImageDate, prevImageDateIsSequence, prevImageGroupHasDate, prevImageDateIsSequence, hasSecondaryImage, imageSequenceInterval, prevImageGroupDate
    set currentImageID to 0
    set prevImageGroupDate to missing value
    set prevImageGroupHasDate to false
    set prevImageDateIsSequence to false
    set imageSequenceInterval to missing value
    set hasSecondaryImage to missing value
    set evScale to 0.69314718056 / aeBracketStep -- round ev offsets to 1/6 of a stop, for both ⅓ stop bracketting and ½ stop bracketting

    global maxSequenceInterval, countSelectedVariants
    local indxVariant, hasImageDate, varImageDate, thisImageDateIsSequence, validExt, varExtID, varExtPrio

    set indxVariant to 0
    repeat countSelectedVariants times
    set indxVariant to indxVariant + 1
    if indxVariant > countSelectedVariants then exit repeat
    set {validExt, varExtID, varExtPrio} to findExtensionsID((piExtensionList's item indxVariant), sortedExtensionsList)
    if debugLogEnable then log {"Main Extension Check", "Valid", validExt, "ID", varExtID, "Prio", varExtPrio, "indxVariant", indxVariant}
    if validExt then

    local varImageDate, hasImageDate
    try
    set varImageDate to piDateList's item indxVariant as date
    set hasImageDate to true
    if debugLogEnable then log {"Main Date Check", hasImageDate}
    on error
    if prevImageGroupHasDate then error "variant sorting issue"
    set hasImageDate to false
    if debugLogEnable then log {"Main Date Check", hasImageDate}
    set imageSequenceInterval to missing value
    set prevImageGroupDate to missing value
    set prevImageGroupHasDate to false
    end try

    if hasImageDate then
    local thisImageDateIsSequence
    if prevImageGroupHasDate then
    set imageSequenceInterval to (varImageDate - prevImageGroupDate)
    end if

    local seqPrimaryImg_L, seqSecondaryImg_L, seqEV_L, dateGrpPrimaryImg_L, dateGrpSecondaryImg_L, dateGrpBn_L, dateGrpNameLen_L, dateGrpEv_L, dateGrpExtID_L, dateGrpExtPrio_L
    if (not prevImageGroupHasDate) or (imageSequenceInterval > 0) then
    ## New image
    ## New Date

    if debugLogEnable then
    if prevImageGroupHasDate then
    log {"Main ", "prevImageGroupHasDate", prevImageGroupHasDate, "imageSequenceInterval", imageSequenceInterval, "prevImageDateIsSequence", prevImageDateIsSequence}
    else
    log {"Main ", "prevImageGroupHasDate", prevImageGroupHasDate, "prevImageDateIsSequence", prevImageDateIsSequence}
    end if
    end if

    if prevImageDateIsSequence then
    if debugLogEnable then log {"Main ", "recordDateGroup"}
    recordDateGroup given seqPrimary_L:seqPrimaryImg_L, seqSecondary_L:seqSecondaryImg_L, seqEV_L:seqEV_L, seqBN_L:seqBN_L, dateGrpPrimary_L:dateGrpPrimaryImg_L, dateGrpSecondary_L:dateGrpSecondaryImg_L, dateGrpBn_L:dateGrpBn_L, dateGrpEv_L:dateGrpEv_L, Intervals:imageSequenceInterval, Interval_L:seqInterval_L
    end if

    if (not prevImageGroupHasDate) or ((not enableIntervalSurvey) and (imageSequenceInterval > maxSequenceInterval)) then -- report the sequence and start over
    if prevImageDateIsSequence then -- previous sequence has ended, report it
    if debugLogEnable then log {"Main ", "reportSequence"}
    if (2 < (count of seqPrimaryImg_L)) then reportSequence given PrimaryImage_L:seqPrimaryImg_L, SecondaryImage_L:seqSecondaryImg_L, ImageEV_L:seqEV_L, seqBN_L:seqBN_L, Intervals:seqInterval_L, piName:piName, piVarRefList:piVarRefList, varNameList:varNameList
    end if
    if debugLogEnable then log {"Main ", "initialise sequence variables"}
    set {seqPrimaryImg_L, seqSecondaryImg_L, seqEV_L, seqBN_L, seqInterval_L} to {{}, {}, {}, {}, {}} -- initialise sequence variables
    set {thisImageDateIsSequence, indxVariant, varImageDate} to findSequenceStart(indxVariant, varImageDate, piDateList, (sortedExtensionsList's item 1), piExtensionList)
    if not thisImageDateIsSequence then exit repeat -- did not find another sequence start
    set {validExt, varExtID, varExtPrio} to findExtensionsID((piExtensionList's item indxVariant), sortedExtensionsList)
    end if

    ## start new date group
    if debugLogEnable then log {"Main ", "start new date group"}
    set {dateGrpPrimaryImg_L, dateGrpSecondaryImg_L, dateGrpBn_L, dateGrpNameLen_L, dateGrpEv_L, dateGrpExtID_L, dateGrpExtPrio_L} to ¬
    {{}, {}, {}, {}, {}, {}, {}}

    ## start new image group
    if debugLogEnable then log {"Main ", "start new image group"}
    set {end of dateGrpPrimaryImg_L, end of dateGrpSecondaryImg_L} to {indxVariant, {}}
    set {end of dateGrpBn_L, end of dateGrpNameLen_L} to {(parseBaseName(varNameList's item indxVariant, basicNameParse)), (count of varNameList's item indxVariant)}
    set {end of dateGrpExtID_L, end of dateGrpExtPrio_L} to {varExtID, varExtPrio}
    set end of dateGrpEv_L to missing value
    copy (piId's item indxVariant as integer) to currentImageID
    copy {varImageDate, true, thisImageDateIsSequence} to {prevImageGroupDate, prevImageGroupHasDate, prevImageDateIsSequence}
    set {igPtr, hasSecondaryImage} to {1, false} -- maintain consistency

    else
    ## Existing Date
    if debugLogEnable then log {"Main ", "Existing Date"}
    local varImageID, varBn, varNameLength, varExtID, varExtPrio, igPtr
    set varImageID to (piId's item indxVariant as integer)
    if varImageID ≠ currentImageID then --Second or higher Variant ignored
    if debugLogEnable then log {"Main ", "Not a variant - New image, get data"}
    copy varImageID to currentImageID -- CO reports all variants of an image in sequence
    ## New image, get data
    set {varBn, varNameLength} to {parseBaseName(varNameList's item indxVariant, basicNameParse), (count of varNameList's item indxVariant)}
    ## Check for basic name match
    repeat with igPtr from (count of dateGrpBn_L) to 0 by -1
    if (0 = (contents of igPtr)) or (varBn = (contents of dateGrpBn_L's item igPtr)) then exit repeat
    end repeat
    set igPtr to (contents of igPtr) -- dereference

    if (0 = igPtr) then -- basic name not found, start new image group
    if debugLogEnable then log {"Main ", "basic name not found, start new image group"}
    set {end of dateGrpPrimaryImg_L, end of dateGrpSecondaryImg_L} to {indxVariant, {}}
    set {end of dateGrpBn_L, end of dateGrpNameLen_L} to {varBn, varNameLength}
    set {end of dateGrpExtID_L, end of dateGrpExtPrio_L} to {varExtID, varExtPrio}
    set end of dateGrpEv_L to missing value
    set {igPtr, hasSecondaryImage} to {(count of dateGrpPrimaryImg_L), false} -- maintain consistency

    else -- add Image to the identified Image group
    if debugLogEnable then log {"Main ", "add Image to the identified Image group"}
    set hasSecondaryImage to (0 < (count of dateGrpSecondaryImg_L's item igPtr))
    if (varExtPrio > (contents of dateGrpExtPrio_L's item igPtr)) or ¬
    ((varExtPrio = (contents of dateGrpExtPrio_L's item igPtr)) and (varNameLength ≥ (contents of dateGrpNameLen_L's item igPtr))) ¬
    then -- if inferior (extension priority, name length) add this image as a secondary image
    if debugLogEnable then log {"Main ", "add this image as a secondary image"}
    if hasSecondaryImage then
    set end of dateGrpSecondaryImg_L's item igPtr to indxVariant
    else
    set dateGrpSecondaryImg_L's item igPtr to {indxVariant}
    end if
    else -- make this the primary image, make the current primary image a secondary image
    if debugLogEnable then log {"Main ", "add this image as the primary image"}
    if hasSecondaryImage then
    copy (dateGrpPrimaryImg_L's item igPtr) to end of dateGrpSecondaryImg_L's item igPtr
    else
    copy {dateGrpPrimaryImg_L's item igPtr} to dateGrpSecondaryImg_L's item igPtr
    end if
    set (dateGrpPrimaryImg_L's item igPtr) to indxVariant
    set (dateGrpExtID_L's item igPtr) to varExtID
    set (dateGrpExtPrio_L's item igPtr) to varExtPrio
    set (dateGrpNameLen_L's item igPtr) to varNameLength
    end if
    end if
    end if
    end if
    end if
    end if
    end repeat

    ## handle the end of the list of selected variants
    if prevImageDateIsSequence then
    if debugLogEnable then log {"Main ", "recordDateGroup"}
    recordDateGroup given seqPrimary_L:seqPrimaryImg_L, seqSecondary_L:seqSecondaryImg_L, seqEV_L:seqEV_L, seqBN_L:seqBN_L, dateGrpPrimary_L:dateGrpPrimaryImg_L, dateGrpSecondary_L:dateGrpSecondaryImg_L, dateGrpBn_L:dateGrpBn_L, dateGrpEv_L:dateGrpEv_L, Intervals:missing value, Interval_L:seqInterval_L
    end if

    if enableIntervalSurvey or (prevImageDateIsSequence and (2 < (count of seqPrimaryImg_L))) then
    if enableSequenceReport then reportSequence given PrimaryImage_L:seqPrimaryImg_L, SecondaryImage_L:seqSecondaryImg_L, ImageEV_L:seqEV_L, seqBN_L:seqBN_L, Intervals:seqInterval_L, piName:piName, piVarRefList:piVarRefList, varNameList:varNameList
    end if
    end mainHandler

    on getAllVariantData()
    global debugLogEnable, selectedCollectionRef, enableHueristicBracket
    set {piIsoList, piApertureList, piShutterList} to {{}, {}, {}}
    tell application "Capture One 12"
    tell selectedCollectionRef to set sorting order to by date
    set {piName, piId, piExtensionList, piDateList, piVarRefList} to ¬
    {name, id, extension, EXIF capture date, variants} of (parent image of every variant whose selected is true)
    set {varNameList} to {name} of (every variant whose selected is true)
    if enableHueristicBracket then set {piIsoList, piApertureList, piShutterList} to {EXIF ISO, EXIF aperture, EXIF shutter speed} of (parent image of every variant whose selected is true)
    end tell
    return {piName:piName, piId:piId, piExtensionList:piExtensionList, piDateList:piDateList, piVarRefList:piVarRefList, varNameList:varNameList, piIsoList:piIsoList, piApertureList:piApertureList, piShutterList:piShutterList}
    end getAllVariantData

    on makeExtensionsList(variantsExtensionList)
    global debugLogEnable, enableRawSequence, enabledRawFileTypeList, enableJpegSequence, enableTiffSequence
    local fileExtensionsList, extPriorityList
    set {fileExtensionsList, extPriorityList} to {{}, {}}
    if enableRawSequence then
    repeat with theFileType in variantsExtensionList
    if enabledRawFileTypeList contains theFileType then
    copy contents of theFileType to end of fileExtensionsList
    copy 1 to end of extPriorityList
    end if
    end repeat
    end if
    if enableJpegSequence and (variantsExtensionList contains "JPG") then ¬
    set {fileExtensionsList, extPriorityList} to {(fileExtensionsList & {"JPG"}), (extPriorityList & {2})}
    if enableTiffSequence then
    if (variantsExtensionList contains "TIF") and (fileExtensionsList does not contain "TIF") then ¬
    set {fileExtensionsList, extPriorityList} to {(fileExtensionsList & {"TIF"}), (extPriorityList & {3})}
    if (variantsExtensionList contains "TIFF") and (fileExtensionsList does not contain "TIFF") then ¬
    set {fileExtensionsList, extPriorityList} to {(fileExtensionsList & {"TIFF"}), (extPriorityList & {3})}
    end if
    if debugLogEnable then log {"Exit makeExtensionsList", "fileExtensionsList", fileExtensionsList, "extPriorityList", extPriorityList}
    return {fileExtensionsList, extPriorityList}
    end makeExtensionsList

    on findExtensionsID(theExtension, sortedExtensionList)
    global debugLogEnable
    if debugLogEnable then log {"findExtensionsID", "theExtension", theExtension}
    local extCtr
    repeat with extCtr from (count of sortedExtensionList's item 1) to 0 by -1
    if (0 < contents of extCtr) and (theExtension = (contents of sortedExtensionList's item 1's item extCtr)) then exit repeat
    end repeat
    if (0 = (contents of extCtr)) then return {false, missing value, missing value}
    if debugLogEnable then log {"Exit findExtensionsID", "extCtr", (contents of extCtr), "ExtPrio", (contents of sortedExtensionList's item 2's item extCtr)}
    return {true, (contents of extCtr), (contents of sortedExtensionList's item 2's item extCtr)}
    end findExtensionsID

    on findSequenceStart(prevVarIndx, prevImageDate, piDateList, extensionsList, piExtensionList)
    global debugLogEnable, maxSequenceInterval, enableIntervalSurvey
    local maxIndx, nextImageDate, validExt, varExtID, varExtPrio, varIndx

    if debugLogEnable then log {"findSequenceStart", "prevVarIndx", prevVarIndx, "prevImageDate", prevImageDate, "enableIntervalSurvey", enableIntervalSurvey}
    if enableIntervalSurvey then return {true, prevVarIndx, prevImageDate}
    set {maxIndx, varIndx} to {(count of piDateList), (prevVarIndx + 1)}
    if varIndx > maxIndx then return {false, missing value, missing value}
    set validExt to (extensionsList contains (contents of piExtensionList's item varIndx))
    if validExt then set nextImageDate to ((piDateList's item varIndx) as date)
    repeat until (validExt and (nextImageDate - prevImageDate ≤ maxSequenceInterval))
    if validExt then copy {varIndx, nextImageDate} to {prevVarIndx, prevImageDate}
    set varIndx to (varIndx + 1)
    if varIndx ≥ maxIndx then return {false, missing value, missing value}
    set validExt to extensionsList contains (contents of piExtensionList's item varIndx)
    if validExt then copy ((piDateList's item varIndx) as date) to nextImageDate
    end repeat
    if debugLogEnable then log {"Exit findSequenceStart", "Found", true, "varIndx", (varIndx - 1), "Date", prevImageDate}
    return {true, prevVarIndx, prevImageDate}
    end findSequenceStart

    on recordDateGroup given seqPrimary_L:primary_L, seqSecondary_L:secondary_L, seqEV_L:EV_L, dateGrpPrimary_L:dgPrimary_L, dateGrpSecondary_L:dgSecondary_L, dateGrpBn_L:dgBn_L, dateGrpEv_L:dgEvList, seqBN_L:seqBN_L, Intervals:nextSequenceInterval, Interval_L:seqInterval_L
    global debugLogEnable
    if debugLogEnable then log {"recordDateGroup", "DG Items", (count of dgPrimary_L), "Seq Items", (count of primary_L)}

    local dgCtr, sortPtr, sortIndx
    set sortIndx to sortOrder(dgBn_L)
    set dgCount to (count of dgPrimary_L)
    repeat with dgCtr from 1 to dgCount
    set sortPtr to (sortIndx's item dgCtr)
    -- copy {(dgPrimary_L's item sortPtr), (dgSecondary_L's item sortPtr), (dgEvList's item sortPtr), (dgBn_L's item sortPtr)} to {end of primary_L, end of secondary_L, end of EV_L, end of seqBN_L}
    copy (dgPrimary_L's item sortPtr) to end of primary_L
    copy (dgSecondary_L's item sortPtr) to end of secondary_L
    copy (dgEvList's item sortPtr) to end of EV_L
    copy (dgBn_L's item sortPtr) to end of seqBN_L
    if (dgCount < (contents of dgCtr)) then
    set end of seqInterval_L to "0"
    else if (missing value = nextSequenceInterval) then
    set end of seqInterval_L to "No Value "
    else
    set end of seqInterval_L to (nextSequenceInterval as text)
    end if
    end repeat
    if debugLogEnable then log {"Exit recordDateGroup", "Seq Items", (count of primary_L)}
    end recordDateGroup

    on sortOrder(T)
    ## insertion sort optimised for Applescript. Good for short lists.
    ## will sort numbers, text strings and dates
    ## returns the sort order, not the sorted list
    global debugLogEnable
    local ptrList, Ictr, Jctr, Vpivot

    if debugLogEnable then log {"sortOrder", "Item count", (count of T), "Items", T}
    set {ptrList, Ictr} to {{1}, 2}
    repeat while Ictr ≤ (count of T)
    copy (T's item Ictr) to Vpivot
    if Vpivot < (T's item (ptrList's item (Ictr - 1))) then
    copy Ictr - 1 to Jctr
    repeat while (Jctr > 0) and (Vpivot < T's item (ptrList's item Jctr))
    set Jctr to Jctr - 1
    end repeat
    if Jctr > 0 then
    set ptrList to (ptrList's items 1 thru Jctr) & (contents of Ictr) & (ptrList's items (Jctr + 1) thru (Ictr - 1))
    else
    copy (contents of Ictr) to beginning of ptrList
    end if
    else
    copy (contents of Ictr) to end of ptrList
    end if
    set Ictr to Ictr + 1
    end repeat
    if debugLogEnable then log {"Exit sortOrder", "Item count", (count of ptrList), "Items", ptrList}
    return ptrList
    end sortOrder

    on reportSequence given PrimaryImage_L:PrimaryImage_L, SecondaryImage_L:SecondaryImage_L, ImageEV_L:ImageEV_L, seqBN_L:seqBN_L, Intervals:seqInterval_L, piName:piName, piVarRefList:piVarRefList, varNameList:varNameList
    global debugLogEnable
    local seqReport_S, imageCtr, anIndex, theprefix
    if debugLogEnable then log {"reportSequence", "Seq Items", (count of PrimaryImage_L)}


    set seqReport_S to "Sequence Name: " & varNameList's item (PrimaryImage_L's item 1)
    set seqReport_S to seqReport_S & return & "Reference Image " & (item (item 1 of PrimaryImage_L) of piName) & " (" & (count of PrimaryImage_L) & " images)"

    set seqCtr to 1
    repeat with imageCtr from 1 to (count of PrimaryImage_L)
    set seqReport_S to seqReport_S & return & "Image " & seqCtr & ": " & item (item imageCtr of PrimaryImage_L) of piName
    set seqReport_S to seqReport_S & " [" & (count of (item (item imageCtr of PrimaryImage_L) of piVarRefList)) & " var.]"
    if missing value ≠ (contents of item imageCtr of SecondaryImage_L) then
    if 2 ≥ (count of item imageCtr of SecondaryImage_L) then
    set theprefix to ""
    set seqReport_S to seqReport_S & " ("
    repeat with anIndex in (item imageCtr of SecondaryImage_L)
    set seqReport_S to seqReport_S & theprefix & item anIndex of piName
    set seqReport_S to seqReport_S & " [" & (count of (item anIndex of piVarRefList)) & " var.]"
    set theprefix to ", "
    end repeat
    set seqReport_S to seqReport_S & ")"
    else
    set theprefix to return & " "
    set seqReport_S to seqReport_S & theprefix & "Secondary Image List:"
    set theprefix to theprefix & " - "
    repeat with anIndex in (item imageCtr of SecondaryImage_L)
    set seqReport_S to seqReport_S & theprefix & item anIndex of piName
    set seqReport_S to seqReport_S & " [" & (count of (item anIndex of piVarRefList)) & " var.]"
    end repeat
    end if
    end if
    set seqReport_S to seqReport_S & return & " Basic Name: "" & (item imageCtr of seqBN_L) & """
    set seqReport_S to seqReport_S & " Next Interval: " & (item imageCtr of seqInterval_L) & "s"
    set seqCtr to seqCtr + 1
    end repeat
    L's loq_Results5(-1, false, (return & "*** " & seqReport_S))
    end reportSequence

    on setIdBnParsingMethod(theMethod)
    global debugLogEnable
    local theNewRecord, theMode, theGroup, ActionList, anAlist, theID
    copy theMethod to theNewMethod
    repeat with theMode in theNewMethod
    repeat with theGroup in theMode
    repeat with anAlist in theGroup's bnpAct
    if "text" = (get (class of anAlist's item 1) as text) then copy (id of contents of anAlist's item 1) to anAlist's item 1
    end repeat
    end repeat
    end repeat
    return theNewMethod
    end setIdBnParsingMethod

    on parseBaseName(theString, theMethod_L)
    ## Looks long, but execution time is about 20 microseconds
    ## theMethod_L is a list of Methods. Each Method describes a parsing method (a particular way of parsing a string)
    ## Each Method is a list of Groups, each Group is a record that describes how to find the end of a group of characters.
    ## While a group runs each character is copied from theString to the output until the end is found.
    ## The algorithm starts with Group 1 of Method 1. When a Group ends the next one starts
    ## When there are no Groups left or no Characters left, the algorithm stops reading characters and returns the output characters.
    ## The Group record: {bnpAlp, bnpNum, bnpSpa, bnpSym, bnpCnt, bnpBck, bnpAct}:
    ## (bnpAlp, bnpNum, bnpSpa, bnpSym, bnpBck) boolean, (bnpCnt) integer, bnpAct a list of Actions {{X,Y,Z},{X,Y,Z},...}
    ## A group ends upon finding Alphabetic characters (if bnpAlp true), or Numbers (if bnpNum true), or a Space(if bnpSpa true),
    ## or Symbols (if bnpSym true), or finding more than bnpCnt characters (if bnpCnt >0).
    ## If bnpBck is true, when a group ends, the last character becomes part of the next group
    ## bnpAct contains Alists {X,Y,Z}, each describes an action to execute when finding some character. bnpAct= {} results in no actions
    ## X is the character that triggers an action; Y is the Action; Z optionally specifies "how much"
    ## Actions: T-Translate Character to "Z" ; G- Switch to Group Z ; J- Jump ahead Z characters ; M- Switch to Method Z ; I- Increment group by Z
    ## Actions A,B,C,N,S and P set the value of bnpAlp, bnpBck, bnpCnt, bnpNum, bnpSpa and bnpSym
    ## If the Alist has no Z value for M, G or I actions, the Method or Group is incremented by 1, and for action "T" the character is dropped
    ## Switching to Group 0 resets the parser to Group 1 and clears all output. If bnpBck is true, the current character is dropped.
    ## Switching to Method 0 triggers a Method reset, output and input are reset, and then the previous method is incremented
    ## Switching to Method whose number does not exist causes the previous output to be cleared, the remaining input characters are accepted

    global debugLogEnable
    local theCharIDList, stringCount, thisCharsId, parsedNumList, charPointer, safetyCtr
    local Alpha, Numb, Blank, Symbol, MaxCnt, backOneOnGrpInc, ActionList, anAlist, theAction
    local thisMethod, MethodCount, thisGroup, GroupCount, hasMaxCnt, charCount, stringCount
    local triggerMethodInit, triggerGroupInit, triggerGroupInc, validChar, newChars
    local nextGroup, nextMethod, triggerBack1Char, safetyCtr, SafetyLimit, hasPar3, valuePar3


    set {theCharIDList, stringCount, nextMethod, MethodCount, triggerMethodInit, parsedNumList, charPointer, safetyCtr, SafetyLimit} to ¬
    {(get id of theString), (count of theString), 1, (count of theMethod_L), true, {}, 1, 0, ((count of theMethod_L) * (count of theString) * 4)}
    repeat while (charPointer ≤ stringCount + 1) and (safetyCtr < SafetyLimit)
    set safetyCtr to safetyCtr + 1
    if charPointer > stringCount then exit repeat

    if triggerMethodInit then set {nextGroup, GroupCount, triggerGroupInit, triggerMethodInit, thisMethod} to {1, (count of theMethod_L's item nextMethod), true, false, nextMethod}

    if triggerGroupInit then tell theMethod_L's item thisMethod's item nextGroup to set {Alpha, Numb, Blank, Symbol, MaxCnt, hasMaxCnt, backOneOnGrpInc, ActionList, charCount, thisGroup, triggerGroupInit} to ¬
    {its bnpAlp, its bnpNum, its bnpSpa, its bnpSym, its bnpCnt, (0 < its bnpCnt), its bnpBck, its bnpAct, 0, nextGroup, false}

    set {triggerBack1Char, triggerGroupInc, validChar, charCount, newChars} to {false, false, true, charCount + 1, ""}
    set thisCharsId to (contents of theCharIDList's item charPointer)

    if hasMaxCnt and (charCount ≥ MaxCnt) then -- if the counter has triggered, don't check the character groups
    set triggerGroupInc to true
    else -- checking the character
    if ((thisCharsId ≥ 65) and (thisCharsId ≤ 90)) or ((thisCharsId ≥ 97) and (thisCharsId ≤ 122)) or (thisCharsId ≥ 192) then -- a latin character
    if Alpha then set triggerGroupInc to true
    else if ((thisCharsId ≥ 48) and (thisCharsId ≤ 57)) then -- a number
    if Numb then set triggerGroupInc to true
    else if thisCharsId = 32 then --- its a space
    if Blank then set triggerGroupInc to true
    else if thisCharsId > 32 then -- its a symbol
    if Symbol then set triggerGroupInc to true
    end if
    end if

    if triggerGroupInc and backOneOnGrpInc then set triggerBack1Char to true

    repeat with anAlist in ActionList
    if thisCharsId = item 1 of anAlist then
    set {theAction, triggerGroupInc, triggerBack1Char} to {(item 2 of anAlist), false, false}
    if 3 ≤ (count of anAlist) then
    set {valuePar3, hasPar3} to {(contents of item 3 of anAlist), true}
    else
    set {valuePar3, hasPar3} to {missing value, false}
    end if
    if ("A" = theAction) then
    if hasPar3 then set Alpha to (true and valuePar3)
    else if "B" = theAction then --
    if hasPar3 then
    set backOneOnGrpInc to (true and valuePar3)
    else
    set triggerBack1Char to true
    end if
    else if ("C" = theAction) then
    if hasPar3 and (0 ≤ valuePar3) then set {MaxCnt, hasMaxCnt} to {valuePar3, (MaxCnt > 0)}
    else if "G" = theAction then -- new group
    if not hasPar3 then set valuePar3 to 1
    set {triggerGroupInit, triggerBack1Char, nextGroup} to {true, (backOneOnGrpInc or triggerBack1Char), valuePar3}
    else if "I" = theAction then -- increment group
    if not hasPar3 then set valuePar3 to 1
    set {triggerGroupInit, triggerBack1Char, nextGroup} to {true, (backOneOnGrpInc or triggerBack1Char), (thisGroup + valuePar3)}
    else if "J" = theAction then --
    if not hasPar3 then set valuePar3 to 1
    if ((thisGroup ≠ nextGroup) or (thisMethod ≠ nextMethod) or valuePar3) then set charPointer to (charPointer + valuePar3)
    else if "M" = theAction then -- new method
    if not hasPar3 then set valuePar3 to thisMethod + 1
    set {nextMethod, triggerMethodInit, triggerBack1Char} to {valuePar3, true, (backOneOnGrpInc or triggerBack1Char)}
    else if ("N" = theAction) then
    if hasPar3 then set Numb to (true and valuePar3)
    else if ("P" = theAction) then
    if hasPar3 then set Symbol to (true and valuePar3)
    else if "S" = theAction then
    if hasPar3 then set Blank to (true and valuePar3)
    else if "T" = theAction then -- translate/drop this character
    set validChar to false
    if hasPar3 then set parsedNumList to parsedNumList & (get id of valuePar3)
    end if
    end if
    end repeat

    if (0 ≥ nextMethod) then set {nextMethod, triggerMethodInit, charPointer} to {(thisMethod + 1), true, 0}
    if (0 ≥ nextGroup) then
    if (-1 ≥ nextGroup) then set {triggerBack1Char, validChar, charPointer} to {false, false, (charPointer - nextGroup - 1)}
    set {parsedNumList, nextGroup, triggerGroupInit} to {{}, 1, true}
    end if

    if triggerGroupInc then set {nextGroup, triggerGroupInit} to {thisGroup + 1, true}
    if triggerBack1Char then
    set validChar to false
    if (thisGroup ≠ nextGroup) or (thisMethod ≠ nextMethod) then set charPointer to (charPointer - 1)
    end if
    if validChar then set end of parsedNumList to thisCharsId

    if nextMethod > MethodCount then -- replace the output with every remaining character
    if charPointer > 1 then set parsedNumList to (theCharIDList's items charPointer thru stringCount)
    if charPointer ≤ 1 then copy theCharIDList to parsedNumList
    exit repeat
    else if (nextGroup > GroupCount) then
    exit repeat
    end if
    set charPointer to charPointer + 1 -- must be last statement in the the loop
    end repeat
    return (string id parsedNumList)
    end parseBaseName

    on finalCleanup()
    ## clean up the large arrays to avoid a large stack that may prevent AppleScript from saving the script
    ## Nothing here right now because all of the big lists are inside a handler, which will clear the meemory when it exits
    global debugLogEnable, parent_name, Script_Title
    end finalCleanup

    0
  • Eric Valk
    The second part. Copy and paste this into Script Editor's Script Window after the first part.


    ###########################################################################################################################
    ## Capture One General Handlers Version 2019/01/13

    ## Uncomment these next lines if to use these handlers as a Script Library file (".scptd")
    ## use AppleScript version "2.5"
    ## use scripting additions
    ## use U : script "Utilities_1215"
    ## use L : script "Loqqing_1220"
    ## property theMainScript : missing value
    ## if false then true

    on validateCOP5(minCOPversionstr, maxCOPversionstr)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose initialisation handler for scripts using Capture One Pro
    ## Extract and check basic information about the Capture One application

    global debugLogEnable
    local COPProcList, theAppRef, numCOPversion, minCOPversion, maxCOPversion
    local digit_mult, Version_digit, min_digit, max_digit, copVersionStr
    local theAppName, copVersion

    tell application "System Events"
    if debugLogEnable then
    L's loq_Results5(2, false, ("COP Processes:" & (get U's joinListToString((get name of every process whose name begins with "Capture One" and background only is false), ", "))))
    L's loq_Results5(3, false, ("All Processes: " & (get U's joinListToString((get name of every process whose background only is false), ", "))))
    end if
    set COPProcList to every process whose name begins with "Capture One" and background only is false
    if (count of COPProcList) = 0 then return {hasErrors:true, errorText:(get L's loqqed_Error_Halt5("COP is not running"))}
    if (count of COPProcList) ≠ 1 then return {hasErrors:true, errorText:(get L's loqqed_Error_Halt5("Unexpected: >1 COP instances"))}
    set theAppRef to item 1 of COPProcList
    set theAppName to ((get name of theAppRef) as text)
    set copDetailedVersion to get version of my application theAppName
    end tell

    tell application "Capture One 12" to set copVersionStr to (get app version)
    set copVersion to (word -1 of copVersionStr)

    if debugLogEnable then
    L's loq_Results5(2, false, ("theAppName: " & theAppName))
    L's loq_Results5(1, false, copVersionStr)
    L's loq_Results5(2, false, ("Capture One full Version " & copDetailedVersion))
    end if
    tell U's compareVersion(copVersion, minCOPversionstr, maxCOPversionstr) to set {minVersionPass, maxVersionPass} to {its minVersionPass, its maxVersionPass}
    if not minVersionPass then return {hasErrors:true, errorText:(get L's loqqed_Error_Halt5(("This Script does not support Capture One " & copDetailedVersion & " - supported versions are from " & minCOPversionstr & " to " & maxCOPversionstr)))}
    if not maxVersionPass then L's loq_Results5(0, true, ("Caution: Capture One " & copDetailedVersion & " has not been verified yet"))
    return {hasErrors:false, theAppName:theAppName, copVersion:copVersion}
    end validateCOP5

    on validateCOPdoc5(theDocRef, validDocKindList)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose initialisation handler for scripts using Capture One Pro
    ## Extract and check basic information about a document

    global debugLogEnable
    local COPDocKind_s, COPDocKind_p, COPDocName

    if "text" = (get class of theDocRef as text) and (0 = (get count of theDocRef)) then tell application "Capture One 12" to set theDocRef to get current document

    try
    tell application "Capture One 12" to set {COPDocName, COPDocKind_p} to get {name, kind} of theDocRef
    on error errorText number errorNumber
    return {hasErrors:true, errorText:(get L's loqqed_Error_Halt5("The Script could not retrieve Capture One document info. Error " & errorNumber & ": "" & errorText & """))}
    end try
    set COPDocKind_s to convertKindList(COPDocKind_p)

    if validDocKindList does not contain COPDocKind_s then return {hasErrors:true, errorText:(get L's loqqed_Error_Halt5((COPDocName & " is a " & COPDocKind_s & " -- not supported by this script")))}
    return {hasErrors:false, COPDocName:COPDocName, COPDocKind_s:COPDocKind_s}
    end validateCOPdoc5

    on validateCOPcollections5(theDocRef)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose initialisation handler for scripts using Capture One Pro
    ## Extract basic information regarding the current collection, and the top level collections
    global debugLogEnable
    local namesTopCollections, kindsTopCollections_s, countTopCollections, selectedCollectionRef, selectedCollectionIndex, kindSelectedCollection_s, nameSelectedCollection

    tell application "Capture One 12" to set {COPDocName, COPDocKind_p} to get {name, kind} of theDocRef
    set COPDocKind_s to convertKindList(COPDocKind_p)

    tell application "Capture One 12" to tell theDocRef
    set selectedCollectionRef to get current collection
    if (missing value = selectedCollectionRef) then
    try
    set current collection to collection "All Images"
    on error
    set current collection to last collection
    end try
    set selectedCollectionRef to get current collection
    end if
    tell selectedCollectionRef to set {nameSelectedCollection, kindSelectedCollection_s} to {name, my convertKindList(kind)}
    set {namesTopCollections, kindsTopCollections_s} to {every collection's name, my convertKindList(every collection's kind)}
    end tell
    set countTopCollections to count of namesTopCollections

    repeat with collectionCounter from 1 to countTopCollections
    if (nameSelectedCollection = item collectionCounter of namesTopCollections) and ¬
    (kindSelectedCollection_s = item collectionCounter of kindsTopCollections_s) then
    set selectedCollectionIndex to collectionCounter
    exit repeat
    end if
    end repeat

    local selectedCollectionMirroredAtTopLast, bottomUserCollectionIndex, topUserCollectionIndex, countFavoriteCollections, namesFavoriteCollections

    if COPDocKind_s = "catalog" then
    repeat with collectionCounter from countTopCollections to 1 by -1
    if ("in Catalog" = item collectionCounter of namesTopCollections) and ¬
    ("smart album" = item collectionCounter of kindsTopCollections_s) then
    set topUserCollectionIndex to collectionCounter - 1
    exit repeat
    end if
    end repeat
    repeat with collectionCounter from 1 to countTopCollections
    if ("Trash" = item collectionCounter of namesTopCollections) and ¬
    ("smart album" = item collectionCounter of kindsTopCollections_s) then
    set bottomUserCollectionIndex to collectionCounter + 1
    exit repeat
    end if
    end repeat

    set selectedCollectionMirroredAtTopLast to ¬
    (selectedCollectionIndex = countTopCollections) and ¬
    ({"catalog folder", "favorite"} does not contain last item of kindsTopCollections_s)

    set {countFavoriteCollections, namesFavoriteCollections} to {missing value, missing value}

    else if COPDocKind_s = "session" then
    repeat with collectionCounter from countTopCollections to 1 by -1
    if ("favorite" ≠ item collectionCounter of kindsTopCollections_s) then
    set topUserCollectionIndex to collectionCounter
    exit repeat
    end if
    end repeat
    repeat with collectionCounter from 1 to countTopCollections
    if ("Trash" = item collectionCounter of namesTopCollections) and ¬
    ("favorite" = item collectionCounter of kindsTopCollections_s) then
    set bottomUserCollectionIndex to collectionCounter + 1
    exit repeat
    end if
    end repeat

    set countFavoriteCollections to countTopCollections - topUserCollectionIndex
    if 0 = countFavoriteCollections then
    set namesFavoriteCollections to {}
    else
    set namesFavoriteCollections to (get items (topUserCollectionIndex + 1) thru countTopCollections of namesTopCollections)
    end if

    set selectedCollectionMirroredAtTopLast to false
    end if

    local selectedCollectionIsUser, namesTopUserCollections, kindsTopUserCollections_s, countTopUserCollections
    set selectedCollectionIsUser to ¬
    (selectedCollectionMirroredAtTopLast or ¬
    ((selectedCollectionIndex ≥ bottomUserCollectionIndex) and (selectedCollectionIndex ≤ topUserCollectionIndex)))

    if topUserCollectionIndex < bottomUserCollectionIndex then
    set {topUserCollectionIndex, bottomUserCollectionIndex} to {missing value, missing value}
    set {namesTopUserCollections, kindsTopUserCollections_s, countTopUserCollections} to {{}, {}, 0}
    else
    set {namesTopUserCollections, kindsTopUserCollections_s} to {(get items bottomUserCollectionIndex thru topUserCollectionIndex of namesTopCollections), (get items bottomUserCollectionIndex thru topUserCollectionIndex of kindsTopCollections_s)}
    set countTopUserCollections to count of namesTopUserCollections
    end if

    return {hasErrors:false, namesTopUserCollections:namesTopUserCollections, kindsTopUserCollections_s:kindsTopUserCollections_s, countTopUserCollections:countTopUserCollections, selectedCollectionRef:selectedCollectionRef, selectedCollectionIndex:selectedCollectionIndex, kindSelectedCollection_s:kindSelectedCollection_s, nameSelectedCollection:nameSelectedCollection, selectedCollectionMirroredAtTopLast:selectedCollectionMirroredAtTopLast, selectedCollectionIsUser:selectedCollectionIsUser, bottomUserCollectionIndex:bottomUserCollectionIndex, topUserCollectionIndex:topUserCollectionIndex, countFavoriteCollections:countFavoriteCollections, namesFavoriteCollections:namesFavoriteCollections}

    end validateCOPcollections5

    on convertKindList(kind_list)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General Purpose Handler for scripts using Capture One Pro
    ## Capture One returns the chevron form of the "kind" property when AppleScript is run as an Application
    ## Unless care is taken to avoid text conversion of this property, this bug breaks script decisions based on "kind"
    ## This script converts text strings with the chevron form to strings with the expected text form
    ## The input may be a single string, a single enum, a list of strings or a list of enums
    ## The code is not compact but runs very fast, between 60us and 210us per item

    set kind_s_list to {}
    set input_is_list to ("list" = (get (class of kind_list) as text))
    if input_is_list then
    if ("text" = (get (class of item 1 of kind_list) as text)) and ¬
    ("«" ≠ (get text 1 of item 1 of kind_list)) then return kind_list -- quick pass through if first item is OK
    repeat with theItem in kind_list
    tell application "Capture One 12" to set the end of kind_s_list to (get theItem as text)
    end repeat
    if ("«" ≠ (get text 1 of item 1 of kind_s_list)) then return kind_s_list
    else
    if ("text" = (get (class of kind_list) as text)) and ¬
    ("«" ≠ (get text 1 of kind_list)) then return kind_list -- quick pass through if first item is OK
    tell application "Capture One 12" to set kind_s1 to (get kind_list as text)
    if "«" ≠ (get text 1 of kind_s1) then return kind_s1 -- quick pass through if input is OK
    set kind_s_list to {kind_s1}
    end if

    set fail_flag to false
    set code_start to -5

    set kind_list to {}
    repeat with Kind_s_item in kind_s_list
    if ("«" ≠ (get text 1 of Kind_s_item)) or (16 < (count of Kind_s_item)) then ¬
    error (get L's loqqed_Error_Halt5("convertKindList received an unexpected Kind string: " & Kind_s_item))

    set kind_code to get (text code_start thru (code_start + 3) of Kind_s_item)
    set kind_type to get (text code_start thru (code_start + 1) of Kind_s_item)

    if kind_type = "CC" then ## Collection Kinds
    if kind_code = "CCpj" then
    set the end of kind_list to "project"
    else if kind_code = "CCgp" then
    set the end of kind_list to "group"
    else if kind_code = "CCal" then
    set the end of kind_list to "album"
    else if kind_code = "CCsm" then
    set the end of kind_list to "smart album"
    else if kind_code = "CCfv" then
    set the end of kind_list to "favorite"
    else if kind_code = "CCff" then
    set the end of kind_list to "catalog folder"
    else
    set fail_flag to true
    end if

    else if kind_type = "CL" then ## Layer Kinds
    if kind_code = "CLbg" then
    set the end of kind_list to "background"
    else if kind_code = "CLnm" then
    set the end of kind_list to "adjustment"
    else if kind_code = "CLcl" then
    set the end of kind_list to "clone"
    else if kind_code = "CLhl" then
    set the end of kind_list to "heal"
    else
    set fail_flag to true
    end if

    else if kind_type = "CR" then ## Watermark Kinds
    if kind_code = "CRWn" then
    set the end of kind_list to "none"
    else if kind_code = "CRWt" then
    set the end of kind_list to "textual"
    else if kind_code = "CRWi" then
    set the end of kind_list to "imagery"
    else
    set fail_flag to true
    end if

    else if kind_type = "CO" then ## Document Kinds
    if kind_code = "COct" then
    set the end of kind_list to "catalog"
    else if kind_code = "COsd" then
    set the end of kind_list to "session"
    else
    set fail_flag to true
    end if
    else
    set fail_flag to true
    end if

    if fail_flag then ¬
    error (get L's loqqed_Error_Halt5("convertKindList received an unexpected Kind string: " & Kind_s_item))

    end repeat

    if input_is_list then
    return kind_list
    else
    return item 1 of kind_list
    end if

    end convertKindList

    ###########################################################################################################################
    ## Logging Handlers Version 2019/01/13

    ## Uncomment these next lines if to use these handlers as a Script Library file (".scptd")
    ## use AppleScript version "2.5"
    ## use scripting additions
    ## use U : script "Utilities_1215"
    ## property theMainScript : missing value
    ## if false then true

    on setupLoqqing5()
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## Handler to setup the variables used by the loqqing system at the start of the script

    global debugLogEnable, Loqqing, loqResultDocRef, loqResultMethod, loqDialogTextList
    ## loqResultDocRef is kept separate from Loqqing to allow easy listing of Loqqing

    ## Default values are entered
    set {loqResultDocRef, loqResultMethod} to {false, "not Initialized"}
    set Loqqing to {stateResultDoc:false, gateResultsFile:false, enableResultsFile:false, initResultDoc:false, ResultsFileMaxDebug:0, debugLogLevel:0}
    set Loqqing to Loqqing & {stateResultsByClipboard:false, gateResultsByClipboard:true, enableResultsByClipboard:true}
    set Loqqing to Loqqing & {stateResultsByDialog:false, gateResultsDialog:true, enableResultsByDialog:false, maxDialogPercent:50, maxDialogLines:25, maxDialogChar:1000}
    set Loqqing to Loqqing & {stateResultsByNotification:false, gateResultsNotification:true, enableResultsByNotifications:false, enableNotifications:true, notificationsMaxDebug:0}
    set Loqqing to Loqqing & {stateResultsByLoq:false, gateParentLoqqing:false, enableResultsByLoq:true, initLoqqing:false, gateLoqqing:true}

    end setupLoqqing5

    on InitializeLoqqing5(DocName_Ext, sourceTitle)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## Handler to initialize logging of results
    ## Do use loq_Results() until the end of the handler and initialising is completed
    ## Doesn't set loqResultMethod, this is set by caller of this handler

    global parent_name, Script_Title, Loqqing, gateScriptProgress, loqDialogTextList, loqResultDocRef

    local LogMethods, LogHeader, date_string
    tell current application to set date_string to (current date) as text
    set LogMethods to {}
    set LogHeader to (sourceTitle & " results on " & date_string)

    local targetFileWasCreated, TextEditlist, createdLine
    if Loqqing's gateResultsFile and Loqqing's enableResultsFile then
    set end of LogMethods to DocName_Ext
    if not Loqqing's initResultDoc then
    set targetFileWasCreated to false
    set Loqqing's stateResultDoc to false
    set Loqqing's initResultDoc to true

    ## If TextEdit is already open and has the document open then add the headerline
    tell application "System Events" to set TextEditlist to get background only of every application process whose name is "TextEdit"
    if (0 < (count of TextEditlist)) and not item 1 of TextEditlist then
    if (DocName_Ext is in (get name of documents of application "TextEdit")) then
    tell application "TextEdit" to tell document DocName_Ext
    set loqResultDocRef to it
    tell its text to set paragraph (1 + (count paragraphs)) to return & LogHeader & return
    set Loqqing's stateResultDoc to true
    end tell
    end if
    end if
    end if

    local targetFolderParent_a, targetFolderParent_p, targetFolderName, targetFolder_a, targetFolder_p, ResultDocPath_a, ResultDocPath_p, newFolderRef, newFileRef
    if not Loqqing's stateResultDoc or (false = loqResultDocRef) then
    -- create the document and the folder if necessary
    -- Do not use finder to test for the file existence because it has a bug that ignores leading 0's
    -- https://www.macscripter.net/viewtopic.php?id=45178
    set targetFolderParent_a to alias (get path to desktop folder as text)
    set targetFolderParent_p to get POSIX path of targetFolderParent_a
    set targetFolderName to "ScriptReports"
    set targetFolder_p to (targetFolderParent_p & targetFolderName)
    set ResultDocPath_p to targetFolder_p & "/" & DocName_Ext

    try
    set ResultDocPath_a to (get alias POSIX file ResultDocPath_p)
    on error
    try
    set targetFolder_a to (get alias POSIX file targetFolder_p) --x1
    on error
    tell application "Finder" to set newFolderRef to make new folder at targetFolderParent_a with properties {name:targetFolderName}
    set targetFolder_a to newFolderRef as alias
    end try
    tell application "Finder" to set newFileRef to make new file at targetFolder_a with properties {name:DocName_Ext}
    set ResultDocPath_a to newFileRef as alias
    set targetFileWasCreated to true
    end try

    set createdLine to ("Created by " & Script_Title & " on " & date_string)
    tell application "TextEdit" -- open the document and add the first line if empty
    activate
    set loqResultDocRef to open ResultDocPath_a
    tell text of loqResultDocRef
    if targetFileWasCreated then
    set paragraph 1 to createdLine & return & return
    else
    if (0 = (count of paragraphs)) then set paragraph 1 to createdLine & return & return
    end if
    end tell
    if 2 ≤ Loqqing's debugLogLevel then tell me to log ResultDocPath_p & ": " & createdLine
    end tell

    set Loqqing's stateResultDoc to true -- prevents initialisation from repeating
    tell application "TextEdit" to tell text of loqResultDocRef to ¬
    set paragraph (1 + (count paragraphs)) to return & LogHeader & return
    end if
    end if
    if Loqqing's gateResultsFile and Loqqing's stateResultDoc and not Loqqing's enableResultsFile then
    set Loqqing's stateResultDoc to false
    tell application "TextEdit" to tell text of loqResultDocRef to ¬
    set paragraph (1 + (count paragraphs)) to return & parent_name & "Results reporting disabled for: " & Script_Title & return
    end if

    local screenWidthO, screenHeightO, screenWidth, screenHeight, fontSize_pts, charactersPerLine, dotsPer_Point, linesPerScreen, borderLines
    if Loqqing's gateResultsDialog and Loqqing's enableResultsByDialog then
    set end of LogMethods to "Dialogs"
    if not Loqqing's stateResultsByDialog then
    set loqDialogTextList to (get LogHeader & return)
    set Loqqing's stateResultsByDialog to true
    end if
    tell application "Finder" to set {screenWidthO, screenHeightO, screenWidth, screenHeight} to bounds of window of desktop
    set fontSize_pts to 12 -- estimated font size for display dialog, including line spacing
    set charactersPerLine to 58 -- estimated characters per line fo display dialog, if there are no "return" characters
    set dotsPer_Point to 1.5 -- similar for 5K 27" iMac and 11" MBA - similar for others - retina independent
    set linesPerScreen to (screenHeight - screenHeightO) / (fontSize_pts * dotsPer_Point)
    set borderLines to 5
    set Loqqing's maxDialogLines to (get ((Loqqing's maxDialogPercent) / 100 * (linesPerScreen - borderLines)) as integer)
    set Loqqing's maxDialogChar to (get ((Loqqing's maxDialogPercent) / 100 * (linesPerScreen - borderLines) * charactersPerLine) as integer)
    if 2 ≤ Loqqing's debugLogLevel then log {"maxDialogPercent", Loqqing's maxDialogPercent, "screenHeight", screenHeight, "linesPerScreen", linesPerScreen, "maxDialogLines", Loqqing's maxDialogLines, "maxDialogChar", Loqqing's maxDialogChar}
    end if
    if Loqqing's gateResultsDialog and Loqqing's stateResultsByDialog and not Loqqing's enableResultsByDialog then
    set Loqqing's stateResultsByDialog to false
    set loqDialogTextList to ""
    end if

    if Loqqing's gateResultsByClipboard and Loqqing's enableResultsByClipboard then
    set end of LogMethods to "Clipboard"
    if not Loqqing's stateResultsByClipboard then
    set the clipboard to LogHeader
    set Loqqing's stateResultsByClipboard to true
    end if
    end if
    if Loqqing's gateResultsByClipboard and Loqqing's stateResultsByClipboard and not Loqqing's enableResultsByClipboard then
    set the clipboard to {}
    set Loqqing's stateResultsByClipboard to false
    end if

    global gateScriptProgress
    if not Loqqing's initLoqqing then
    set Loqqing's initLoqqing to true
    if ("Script Editor" = parent_name) then
    set Loqqing's gateParentLoqqing to true
    set Loqqing's gateLoqqing to true
    set gateScriptProgress to true
    try -- Open the Log History window
    tell application "System Events" to tell application process "Script Editor"
    if (get name of windows) does not contain "log History" then ¬
    click menu item "Log History" of menu "Window" of menu bar 1
    end tell
    end try
    else if ("Script Debugger" = parent_name) then
    set Loqqing's gateParentLoqqing to false
    set Loqqing's gateLoqqing to true
    set gateScriptProgress to true
    ## Avoids compiler errors when Script Debugger is not present
    run script "tell application "Script Debugger" to tell first document to set event log visible to true"
    run script "tell application "Script Debugger" to tell first document to set event log scope bar visible to true"
    else
    set Loqqing's gateParentLoqqing to false
    set Loqqing's gateLoqqing to false
    set gateScriptProgress to false
    end if
    end if

    if Loqqing's gateParentLoqqing and Loqqing's enableResultsByLoq then
    set end of LogMethods to " " & parent_name & " Log"
    if not Loqqing's stateResultsByLoq then
    set Loqqing's stateResultsByLoq to true
    log "Results Logging enabled for: " & Script_Title
    end if
    end if
    if Loqqing's gateParentLoqqing and Loqqing's stateResultsByLoq and not Loqqing's enableResultsByLoq then
    set Loqqing's stateResultsByLoq to false
    log "Results Logging disabled for: " & Script_Title
    end if

    if (Loqqing's gateResultsNotification and Loqqing's enableResultsByNotifications) or Loqqing's enableNotifications then
    set end of LogMethods to "Notifications"
    if not Loqqing's stateResultsByNotification then
    display notification "Notifications enabled for: " & Script_Title
    set Loqqing's stateResultsByNotification to true
    end if
    else if Loqqing's stateResultsByNotification then
    display notification "Notifications disabled for: " & Script_Title
    set Loqqing's stateResultsByNotification to false
    end if

    set LogMethods_S to U's joinListToString(LogMethods, ", ")
    if 2 ≤ Loqqing's debugLogLevel then loq_Results5(2, false, ("Result Reported by " & LogMethods_S))
    return LogMethods_S
    end InitializeLoqqing5

    on loq_Results5(thisLogDebugLevel, MakeFront, log_Text)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose handler for logging results
    ## log results if the debug level of the message is below the the threshold set by debugLogLevel
    ## log the results by whatever mechanism is ebabled - {Script Editor Log, Text Editor Log, Display Dialog}

    global parent_name, Loqqing, loqDialogTextList, loqResultDocRef
    local log_Text_S

    if thisLogDebugLevel ≤ Loqqing's debugLogLevel then
    set log_Text_S to U's joinListToString(log_Text, "; ")
    set triggeredDialog to false

    if (thisLogDebugLevel = 0) then -- Critical errors
    if Loqqing's gateLoqqing then log (log_Text_S)
    if Loqqing's enableResultsByClipboard then set the clipboard to ((get the clipboard) & return & (log_Text_S as text))
    if Loqqing's enableResultsFile then ¬
    tell application "TextEdit" to tell text of loqResultDocRef to ¬
    set paragraph (1 + (count paragraphs)) to (log_Text_S & return)
    if Loqqing's enableResultsByDialog then display alert log_Text_S giving up after 10
    (Loqqing's enableResultsByNotifications and (thisLogDebugLevel ≤ 0))
    else if (thisLogDebugLevel ≤ 0) then -- Expected Results
    if Loqqing's enableResultsByLoq then log (log_Text_S)
    if Loqqing's enableResultsByClipboard then set the clipboard to ((get the clipboard) & return & (log_Text_S as text))

    if Loqqing's enableResultsFile then ¬
    tell application "TextEdit" to tell text of loqResultDocRef to ¬
    set paragraph (1 + (count paragraphs)) to (log_Text_S & return)

    if Loqqing's enableResultsByDialog then -- process the dialog last
    set loqDialogTextList to (get loqDialogTextList & return & log_Text_S)
    if MakeFront or (0 ≥ Loqqing's maxDialogLines) or ¬
    (Loqqing's maxDialogLines < (get count of paragraphs of loqDialogTextList)) or ¬
    (Loqqing's maxDialogChar < (get length of loqDialogTextList)) then
    --tell application "System Events" to set frontmost of process parent_name to true
    display dialog loqDialogTextList
    set triggeredDialog to true
    set loqDialogTextList to ""
    end if
    end if

    else -- debugging information
    if Loqqing's gateLoqqing then log (log_Text_S)
    if Loqqing's enableResultsFile and ((thisLogDebugLevel ≤ Loqqing's ResultsFileMaxDebug) or not Loqqing's gateLoqqing) then ¬
    tell application "TextEdit" to tell text of loqResultDocRef to ¬
    set paragraph (1 + (count paragraphs)) to ((log_Text_S as text) & return)
    end if

    if (Loqqing's enableNotifications and (thisLogDebugLevel ≤ Loqqing's notificationsMaxDebug)) or ¬
    (Loqqing's enableResultsByNotifications and (thisLogDebugLevel ≤ 0)) then
    set paraCtr to 0
    set notString to ""
    set lineCnt to 39
    set paramax to 3
    copy (get paramax * lineCnt) to remChar
    repeat with thePara in (get paragraphs of log_Text_S)
    set thisCount to (get count of (get contents of thePara))
    if thisCount > 0 then
    set paraCtr to paraCtr + 1
    if thisCount > remChar then copy remChar to thisCount
    set notString to notString & (get text 1 thru thisCount of thePara)
    set remChar to remChar - lineCnt * (thisCount div lineCnt)
    if (0 < (thisCount mod lineCnt)) then set remChar to remChar - lineCnt
    if (paramax ≤ paraCtr) or (0 ≥ remChar) then exit repeat
    set notString to notString & return
    end if
    end repeat
    display notification notString
    end if

    if (not triggeredDialog) and (MakeFront or (0 = thisLogDebugLevel)) then --
    if Loqqing's gateLoqqing then tell application "System Events" to set frontmost of process parent_name to true
    if Loqqing's enableResultsFile then tell application "System Events" to set frontmost of process "TextEdit" to true
    end if

    else
    set log_Text_S to ""
    end if
    return log_Text_S
    end loq_Results5

    on loqqed_Error_Halt5(errorText)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose handler for logging during script termination
    global debugLogEnable, Script_Title
    try
    theMainScript's finalCleanup()
    end try
    tell current application to set date_string to (current date) as text
    return (get loq_Results5(0, true, ("Script "" & Script_Title & "" is exitting at " & date_string & "Reason: " & errorText & return)))
    end loqqed_Error_Halt5

    ###########################################################################################################################
    ## General Utility Handlers Version 2019/01/12
    ## No Dependencies on other Libraries

    on compareVersion(testVersion_S, minVersion_S, maxVersion_S)
    ## Copyright 2019 Eric Valk, Ottawa, Canada Creative Commons License CC BY-SA No Warranty.
    ## General purpose handler for comparing versions
    --local digitMult, testVersionNumber, minVersionNumber, maxVersionNumber, testVersion_LS, minVersion_LS, maxVersion_LS, groupsCount, group_ctr
    --local testGroupsCount, minGroupsCount, maxGroupsCount, hasTestGroup, hasMinGroup, hasMaxGroup, testGroupVersion, minGroupVersion, maxGroupVersion
    --local allBoundsPass, lowerBoundPass, upperBoundPass, lowerBoundFail, upperBoundFail

    if (text ≠ (get class of testVersion_S)) or (text ≠ (get class of minVersion_S)) or (text ≠ (get class of maxVersion_S)) then error "Text inputs only"

    set testVersion_LS to (splitStringToList(removeLeadingTrailingSpaces(testVersion_S), "."))
    set minVersion_LS to (splitStringToList(removeLeadingTrailingSpaces(minVersion_S), "."))
    set maxVersion_LS to (splitStringToList(removeLeadingTrailingSpaces(maxVersion_S), "."))

    set testGroupsCount to get count of testVersion_LS
    set minGroupsCount to get count of minVersion_LS
    set maxGroupsCount to get count of maxVersion_LS

    set groupsCount to testGroupsCount
    if groupsCount < minGroupsCount then set groupsCount to minGroupsCount
    if groupsCount < maxGroupsCount then set groupsCount to maxGroupsCount

    set lowerBoundPass to false
    set lowerBoundFail to false
    set upperBoundPass to false
    set upperBoundFail to false

    repeat with group_ctr from 1 to groupsCount

    set hasTestGroup to (get group_ctr ≤ testGroupsCount)
    set hasMinGroup to (get group_ctr ≤ minGroupsCount)
    set hasMaxGroup to (get group_ctr ≤ maxGroupsCount)

    if hasTestGroup then set testGroupVersion to (get (item group_ctr of testVersion_LS) as integer)
    if hasMinGroup then set minGroupVersion to (get (item group_ctr of minVersion_LS) as integer)
    if hasMaxGroup then set maxGroupVersion to (get (item group_ctr of maxVersion_LS) as integer)

    if not (lowerBoundPass or lowerBoundFail) then
    if hasMinGroup and hasTestGroup then
    if testGroupVersion > minGroupVersion then set lowerBoundPass to true
    if testGroupVersion < minGroupVersion then set lowerBoundFail to true
    else if hasMinGroup then -- (and no testGroup) e.g. test 11 , min 11.1
    set lowerBoundFail to true -- not symetric with upper bound behaviour
    else if hasTestGroup then -- (and no minGroup) e.g. test 11.1, min 11 --> OK
    set lowerBoundPass to true
    end if
    end if

    if not (upperBoundPass or upperBoundFail) then
    if hasMaxGroup and hasTestGroup then
    if testGroupVersion < maxGroupVersion then set upperBoundPass to true
    if testGroupVersion > maxGroupVersion then set upperBoundFail to true
    else if hasMaxGroup then -- (and no testGroup) e.g. test 11.1 , max 11.1.2 or test 11 , max 11.1
    set upperBoundPass to true -- not symetric with lower bound behaviour
    else if hasTestGroup then --(and no maxGroup) e.g. test 11.1, max 11 --> pass
    set upperBoundPass to true
    end if
    end if

    end repeat

    return {maxVersionPass:(not upperBoundFail), minVersionPass:(not lowerBoundFail)}
    end compareVersion

    on deReference(theItem, theclassName)
    ## General purpose handler for removing references from a variable
    ## reusult is a value, or list of values, of the specified class
    ## Enables data handling of multiple classes of items in the same code

    if class = (get class of theclassName) then set theclassName to (get theclassName as text)

    if list = (get class of (get theItem)) then
    set theResult to {}
    set cntItems to length of theItem
    if (text = (get class of theclassName)) then
    set hasClassList to false
    else if (list = (get class of theclassName)) then
    if (1 = (get length of theclassName)) then
    set hasClassList to false
    else if (cntItems = (get length of theclassName)) then
    set hasClassList to true
    else
    error "deReference() can't handle a mismatch between number of items and number of classes"
    end if
    end if
    if not hasClassList then copy (get theclassName as text) to thisclassName
    repeat with item_ctr from 1 to cntItems
    if hasClassList then copy (get (theclassName's item item_ctr) as text) to thisclassName
    copy (get theItem's item item_ctr) to thisItem
    if "boolean" = thisclassName then
    set the end of theResult to false or (get thisItem as boolean)
    else if "integer" = thisclassName then
    set the end of theResult to 0 + (get thisItem as integer)
    else if "text" = thisclassName then
    set the end of theResult to "" & (get thisItem as text)
    else if "real" = thisclassName then
    set the end of theResult to 0.0 + (get thisItem as real)
    else if "date" = thisclassName then
    set the end of theResult to (get thisItem as date) + 0
    else
    error "deReference() can't handle class "" & thisclassName & """
    end if
    end repeat

    else
    if "boolean" = theclassName then
    set theResult to false or (get theItem as boolean)
    else if "integer" = theclassName then
    set theResult to 0 + (get theItem as integer)
    else if "text" = theclassName then
    set theResult to "" & (get theItem as text)
    else if "real" = theclassName then
    set theResult to 0.0 + (get theItem as real)
    else if "date" = theclassName then
    set theResult to (get theItem as date) + 0
    else
    error "deReference() can't handle class "" & theclassName & "" as " & (get class of theclassName)
    end if
    end if
    return theResult
    end deReference

    on makeList(listLength, theElement)
    -- Note that the theElement can even be a List
    if listLength = 0 then return {}
    if listLength = 1 then return {theElement}

    set theList to {theElement}
    repeat while (count of theList) < listLength / 2
    copy contents of theList to ListB
    copy theList & ListB to theList
    end repeat
    copy contents of theList to ListB
    return (theList & items 1 thru (listLength - (count of ListB)) of ListB)
    end makeList

    on splitStringToList(theString, theDelim)
    ## Public Domain
    set theList to {}
    set astid to AppleScript's text item delimiters
    try
    set AppleScript's text item delimiters to theDelim
    set theList to text items of theString
    on error
    set AppleScript's text item delimiters to astid
    end try
    set AppleScript's text item delimiters to astid
    return theList
    end splitStringToList

    on joinListToString(theList, theDelim)
    ## Public Domain
    set theString to ""
    set astid to AppleScript's text item delimiters
    try
    set AppleScript's text item delimiters to theDelim
    set theString to theList as string
    on error
    set AppleScript's text item delimiters to astid
    end try
    set AppleScript's text item delimiters to astid
    return theString
    end joinListToString

    on removeLeadingTrailingSpaces(theListString)
    ## 40% faster than a version which trims the string 1 space at a time
    ## handles both string and list input correctly
    -- local theListString, input_is_list, cleanList, thecount, hasTriggered, indexLow, indexHigh

    set input_is_list to ("list" = (get (class of theListString) as text))
    if not input_is_list then set theListString to {theListString}

    set cleanList to {}
    repeat with theString in theListString
    if ("text" = (get (class of theString) as text)) then
    set thecount to (get count of theString)
    set hasTriggered to false
    repeat with indexLow from 1 to thecount
    if " " ≠ (get text indexLow of theString as text) then
    set hasTriggered to true
    exit repeat
    end if
    end repeat
    if not hasTriggered then
    set theString to ""
    else
    repeat with indexHigh from -1 to (-thecount) by -1
    if " " ≠ (get text indexHigh of theString as text) then exit repeat
    end repeat
    set theString to text indexLow thru indexHigh of theString
    end if
    end if

    set the end of cleanList to theString
    end repeat

    if input_is_list then return cleanList

    return (get contents of theString)
    end removeLeadingTrailingSpaces

    on removeTextFromList(theList, theBadChar)
    ## theBadChar may be a string or character, whereupon that string will be removed
    ## theBadChar may be a list of strings or characters, whereupon those strings will be removed

    set theDelim to character id 60000 -- obscure character chosen for the low likelihood of its appearance
    set astid to AppleScript's text item delimiters
    try
    set AppleScript's text item delimiters to theDelim
    set theString to theList as string
    set AppleScript's text item delimiters to theBadChar
    set text_list to every text item of theString
    set AppleScript's text item delimiters to ""
    set cleanText_S to the text_list as string
    set AppleScript's text item delimiters to theDelim
    set theList to text items of cleanText_S
    on error errorText
    set AppleScript's text item delimiters to astid
    log errorText
    end try
    set AppleScript's text item delimiters to astid

    return theList
    end removeTextFromList

    on removeItemFromList(theList, theBadItem)
    ## theBadChar may be a string or character, whereupon that string will be removed
    ## theBadChar may be a list of strings or characters, whereupon those strings will be removed
    local theDelim, theString, text_list, cleanText_S, errorText, astid
    set theDelim1 to character id 60000 -- obscure characters chosen for the low likelihood of its appearance
    set theDelim2 to character id 60001
    set astid to AppleScript's text item delimiters
    try
    set AppleScript's text item delimiters to {theDelim2 & theDelim1}
    set theString to theDelim1 & (theList as string) & theDelim2
    set AppleScript's text item delimiters to theDelim1 & theBadItem & theDelim2
    set text_list to every text item of theString
    set AppleScript's text item delimiters to ""
    set cleanText_S to text_list as string
    if 2 < (count of cleanText_S) then
    set cleanText_S to text 2 thru -2 of (text_list as string)
    else
    set cleanText_S to ""
    end if
    set AppleScript's text item delimiters to {theDelim2 & theDelim1}
    set theList to text items of cleanText_S
    on error errorText
    set AppleScript's text item delimiters to astid
    log errorText
    end try
    set AppleScript's text item delimiters to astid

    return theList
    end removeItemFromList

    on replaceText(this_text, search_string, replacement_string)
    set astid to AppleScript's text item delimiters
    try
    set AppleScript's text item delimiters to the search_string
    set the item_list to every text item of this_text
    set AppleScript's text item delimiters to the replacement_string
    set this_text to the item_list as string
    end try
    set AppleScript's text item delimiters to astid
    return this_text
    end replaceText

    on getIndexOfStrict(theItem, theList)
    ## based on the idea by Emmanuel Levy, modified to handle embedded non-alphabetic characters correctly
    set theDelim to character id 60000 -- obscure character chosen for the low likelihood of its appearance
    set theReturnToken to character id 60001
    set theSearchItem to return & replaceText(theItem, return, theReturnToken) & return
    set tempString to joinListToString(theList, theDelim)
    set tempString to replaceText(tempString, return, theReturnToken)
    set theSearchList to return & replaceText(tempString, theDelim, return) & return
    try
    -1 + (count (paragraphs of (text 1 thru (offset of theSearchItem in theSearchList) of theSearchList)))
    on error
    0
    end try
    end getIndexOfStrict

    on roundToQuantum(thisValue, quantum)
    ## Public domain author unknown
    return (round (thisValue / quantum) rounding to nearest) * quantum
    end roundToQuantum

    on roundDecimals(n, numDecimals)
    ## Nigel Garvey, Macscripter
    set x to 10 ^ numDecimals
    tell n * x to return (it div 0.5 - it div 1) / x
    end roundDecimals

    on MSduration(firstTicks, lastTicks)
    ## Public domain
    ## returns duration in ms
    ## inputs are durations, in seconds, from GetTick's Now()
    return (round (10000 * (lastTicks - firstTicks)) rounding to nearest) / 10
    end MSduration

    on GetTick_Now()
    ## From MacScripter Author "Jean.O.matiC"
    ## returns duration in seconds since since 00:00 January 2nd, 2000 GMT, calculated using computer ticks
    script GetTick
    property parent : a reference to current application
    use framework "Foundation" --> for more precise timing calculations
    on Now()
    return (current application's NSDate's timeIntervalSinceReferenceDate) as real
    end Now
    end script

    return GetTick's Now()
    end GetTick_Now

    on noValue()
    ## Matt Nueberg
    end noValue

    on isARef(objectToBeTested)
    ## Matt Nueberg
    try
    objectToBeTested as reference
    return true
    on error
    return false
    end try
    end isARef

    on summarizeList(theList, numWords, nullString)
    local safetyCtr, theListLength, summaryList, theItem, newString
    set safetyCtr to 0
    set theListLength to count of theList
    set summaryList to {}
    repeat while 0 < (count of theList)
    set safetyCtr to safetyCtr + 1
    if theListLength < safetyCtr then error
    set theItem to item 1 of theList
    set theList to removeItemFromList(theList, theItem)
    if 0 = (count of theItem) then
    if nullString and summaryList does not contain theItem then copy theItem to end of summaryList
    else if 0 = numWords then
    if summaryList does not contain theItem then copy theItem to end of summaryList
    else if 1 = numWords then
    set newString to word 1 of theItem
    if summaryList does not contain newString then copy newString to end of summaryList
    else
    set lastWord to count of words of theItem
    if numWords < lastWord then set lastWord to numWords
    set newString to joinListToString((words 1 thru lastWord of theItem), " ")
    if summaryList does not contain newString then copy newString to end of summaryList
    end if
    end repeat
    return summaryList
    end summarizeList

    on ln(x, y)
    ## High speed calculation of the natural log of X using Borchardt's algorithm
    ## Y is approximately the digits of precision
    ## https://math.stackexchange.com/questions/75074/an-alternative-way-to-calculate-logx
    ## Chose this approach as it is faster the do shell Unix call

    ## prescaling
    set e3 to 20.085536923188
    set e3i to 0.049787068368
    set v to 0

    if x > e3 then
    repeat while x > e3
    set x to x / e3
    set v to v + 3
    end repeat
    else if x < e3i then
    repeat while x < e3i
    set x to x * e3
    set v to v - 3
    end repeat
    end if

    set z to 10 ^ -y
    set ak to (1 + x) / 2
    set bk to x ^ 0.5
    set ck to ak + bk
    repeat 100 times -- about 9 - 10 times for 6 figure accuracy
    set ak to (ak + bk) / 2
    set bk to (ak * bk) ^ 0.5
    set w to 1 - ((ak + bk) / ck) -- fractional change
    if w < 0 then set w to -w
    if w < z then exit repeat
    set ck to ak + bk
    end repeat

    return v + 2 * (x - 1) / (ak + bk)
    end ln
    0
  • FL
    Thanks Eric, very kind!
    I will try working with the script over the weekend and provide feedback early next week.

    Best
    Frank
    0
  • FL
    Hi Eric,
    gave it a first try. Got

    Expected variable name or property but found “*â€.
    with the cursor on the first star:
    local *censored word*, sortedExtensionsList, extensionCount
    Any suggestions?
    Best
    Frank
    0
  • Eric Valk
    [quote="FL_" wrote:
    Hi Eric,
    gave it a first try. Got

    Expected variable name or property but found “*â€.
    with the cursor on the first star:
    local *censored word*, sortedExtensionsList, extensionCount
    Any suggestions?
    Best
    Frank

    I see what happened, but why has me mystified. I think it may be website content control software gone bonkers

    In my script I use a variable named variant$ExtensionList (with the "$" replaced by "s").

    It appears that some automated process in this website replaces every occurence of this string with *censored word*.

    I tried to write it on the next line, and when I previewed it, it had been changed to *censored word* 😲 😲 😂

    In any case, if you use script editor, and change *censored word* to variantExtensionList (or any string which is a legal Applescript variable name) then the script should compile. There are 9 occurrences in all, type "command-F" script editor this brings up the find and replace bar, and from there you can step through each occurence, and you can replace all of them at once.

    I raised a ticket with Phase One, and double checked the code that I've posted, replacing *censored word* with variant$ExtensionList ("$" means "s")

    it worked for me.
    0
  • FL
    Thanks Eric, that solved it. Now the script is running and I can start playing with it (and provide feedback). Will take some time to work through it as it is quite sophisticated.

    Best
    Frank
    0

Post is closed for comments.