Need Solution to Randomize Large Amounts of Images

Hello Everyone. Basically what I am trying to achieve is the following:

1- Create an alias list of any amount of folders with the Finder’s selection.
2- Get all images from those folders with a shell script.
3- Randomize those images.
4- Open the randomized images in the Preview application.

So far, I got it working properly. The only problem I am having is speed. The randomization part of this script takes a while to process. I have tested it on 150+ images and it takes about 15 seconds to randomize them. If you remove the randomization part, the script executes instantaneously.

I will be using this script on a large amount of images (possibly more than 500 at a time) and I will like for it to execute as fast as possible.

So what I am looking for is a shell script or any other kind of solution that can randomize the all of images quickly instead of using the Finder which is much slower.

Any help is appreciated. Thanks!


tell application "Finder"
	set theFolders to selection as alias list
	
	-- Create list of jpegs within selected folders
	
	set allImages to {} as alias list
	repeat with aFolder in theFolders
		
		set getImages to "find " & aFolder's POSIX path's quoted form & " -iname '*.jpg'"
		set theImages to paragraphs of (do shell script getImages)
		
		-- Get aliases
		
		set filePaths to {}
		repeat with thisFile in theImages
			set end of filePaths to (thisFile as POSIX file as alias)
		end repeat
		
		--set theImages to (files whose name extension is "jpg") of aFolder as alias list
		
		repeat with oneImage from 1 to count of filePaths
			copy item oneImage of filePaths to end of allImages
		end repeat
	end repeat
	
	-- Count number of allImages
	
	set Counter to count allImages
	
	-- Randomize Images
	
	set randomImages to {}
	repeat until (count of randomImages) is Counter
		set someImage to some item of allImages
		if randomImages does not contain someImage then
			copy someImage to end of randomImages
		end if
	end repeat
	
	open randomImages
	
end tell

First of all, you should move all except the first and last lines of that script outside the Finder tell block. The Finder can’t run shell scripts, and it has nothing to do with AppleScript sorting.

Here’s an alternative approach using AppleScriptObjC:

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use framework "AppKit"
use framework "GamePlayKit"
use scripting additions

-- get selection
tell application "Finder" to set theFolders to selection as alias list
-- make mutable array to hold files
set allFiles to current application's NSMutableArray's array()
-- get list of all files in folders
set fileManager to current application's NSFileManager's defaultManager()
set enumerationOptions to ((current application's NSDirectoryEnumerationSkipsPackageDescendants) + (current application's NSDirectoryEnumerationSkipsHiddenFiles as integer))
repeat with aFolder in theFolders
	set theFiles to (fileManager's enumeratorAtURL:aFolder includingPropertiesForKeys:{} options:enumerationOptions errorHandler:(missing value))'s allObjects()
	(allFiles's addObjectsFromArray:theFiles)
end repeat
-- filter just .jpg files
set thePred to current application's NSPredicate's predicateWithFormat:"pathExtension ==[c] 'jpg'"
set theJpegs to allFiles's filteredArrayUsingPredicate:thePred
-- shuffle array and convert to list of files
set theJpegs to (theJpegs's shuffledArray()) as list
--open them
tell application "Finder" to open theJpegs

This vanilla edit includes the changes Shane noted. Expounding a bit… having a shell script inside a tell block throws silent errors that may degrade performance. You also had an unnecessary loop in your code that may have impacted performance. I threw in a my or two, which is some AS reference hocus pocus for vastly improving access speed in positional list statements.

tell application "Finder" to set theFolders to selection as alias list

-- Create list of jpegs within selected folders
set allImages to {}
set filePaths to {}
repeat with aFolder in theFolders
	set theImages to (do shell script "find " & aFolder's POSIX path's quoted form & " -iname '*.jpg' ")'s paragraphs
	--convert to aliases
	repeat with thisFile in theImages
		set end of filePaths to (thisFile as POSIX file as alias)
	end repeat
	--repeat with oneImage from 1 to count of filePaths 
	--copy item oneImage of filePaths to end of allImages
	--end repeat
end repeat
-- Randomize Images
set randomImages to {}
if filePaths is not {} then repeat until (count randomImages) is (count filePaths)
	set someImage to my some item of filePaths
	if randomImages does not contain someImage then
		copy someImage to my end of randomImages
	end if
end repeat

tell application "Finder" to open randomImages

Another suggestion:

tell application "Finder" to set theFolders to selection as alias list

repeat with aFolder in theFolders
	set aFolder to quoted form of POSIX path of aFolder
	
	set theImages to paragraphs of (do shell script "find " & aFolder & " -iname '*.jpg' | sort -R")
	
	repeat with aFile in theImages
		set contents of aFile to POSIX file aFile as alias
	end repeat
	
end repeat

tell application "Finder" to open theImages

In testing with 487 files, my script took 100 milliseconds and Shane’s script took 25 milliseconds. This is with the last line of both scripts removed.

The use of the find command is often problematic because it descends into packages, but that may not be an issue in this case. Regardless, Shane’s script is preferred for speed and reliability.

A big thank you to everyone for the effort and replies. Yes I have tested all scripts posted on this thread and I see that Shane’s AppleScriptObjC approach is the fastest. I will be saving the others just in case.

While I was searching for solutions, I was told to use the Fisher-Yates Shuffle Algorithm as a handler. But it is still slow compared to Shane’s approach. Here is the code if you are interested:

tell application "Finder"
	if not selection is {} then
		set theFolders to selection as alias list
	else
		return
	end if
end tell

set theImages to {}
repeat with aFolder in theFolders
	set getImages to "find " & aFolder's POSIX path's quoted form & " -iname '*.jpg'"
	set tempList to paragraphs of (do shell script getImages)
	repeat with aItem in tempList
		copy aItem to end of theImages
	end repeat
end repeat

set randomImages to shuffle(theImages)

set filePaths to {}
repeat with thisFile in randomImages
	copy (thisFile as POSIX file as alias) to the end of filePaths
end repeat

tell application "Finder" to ¬
	open filePaths using application file id "com.apple.Preview"

-- Handlers (Fisher-Yates Shuffle Algorithm)

on shuffle(input)
	script s
		property L : input
	end script
	set i to count of L of s
	repeat while i ≥ 2
		set j to random number from 1 to i
		set {item i of L of s, item j of L of s} to {item j of L of s, item i of L of s}
		set i to i - 1
	end repeat
	L of s
end shuffle

Little faster should be this (removed needless repeat loop & needless as alias coercion in the other repeat loop, removed also needless mutable array):


tell application "Finder"
	if selection is {} then return
	set theFolders to selection as alias list
end tell

set theImages to {}
repeat with aFolder in theFolders
	set shellCommand to "find " & aFolder's POSIX path's quoted form & " -iname '*.jpg'"
	set theImages to theImages & paragraphs of (do shell script shellCommand)
end repeat

set randomImages to shuffle(theImages)
repeat with aItem in randomImages
	set contents of aItem to aItem as POSIX file
end repeat

tell application "Finder" to open randomImages using application file id "com.apple.Preview"

-- Handlers (Fisher-Yates Shuffle Algorithm)

on shuffle(input)
	script s
		property L : input
	end script
	set i to count of L of s
	repeat while i ≥ 2
		set j to random number from 1 to i
		set {item i of L of s, item j of L of s} to {item j of L of s, item i of L of s}
		set i to i - 1
	end repeat
	L of s
end shuffle

FYI, my code above is using the arc4random function. I’m not sure what Apple’s random number generator uses; it was originally using random, but I hope it’s been updated to arc4random at some stage.For greater or lesser randomization, there are a couple of other options:

-- shuffle array and convert to list of files
set randSource to current application's GKMersenneTwisterRandomSource's new() -- or GKLinearCongruentialRandomSource's
set theJpegs to (randSource's arrayByShufflingObjectsInArray:theJpegs) as list

I was curious if it’s even possible to open 500 JPG images in Preview and ran a test with 467 JPG images, each of which was about 3-MB in size. It took a while, but Preview did open all of these images.

I retested my script from Post 4 with Script Geek but deleted the second alias coercion as suggested by KniazidisR. This reduced the script’s execution time from 100 to 33 milliseconds. I also tested the modified script to insure that Preview would open the files and it did.

Peavine,

Your script sorts the images of all subfolders of a single folder. Here we need to sort the larger list - the images of all subfolders of all folders selected in the Finder, together. Also, the Finder for you opens the images of only the last selected folder. Due to these errors, I took as a basis for improvement not your script, but the last script from OP.

Stanley’s solution is the best in terms of execution speed, but it consumes additional time for a linking the code at the first launch. Therefore, it would be nice to have an effective plain-AppleScript version too. The latter is true for all scripts that should run only 1 time.

KniazidisR. Thanks for catching that. I will rewrite my script to correct the above error and will rerun the tests.

I thought it was a theme of 2d bin packing.

http://piyocast.com/as/archives/6137

I rewrote my script to accommodate multiple selected folders. Hopefully I got it correct this time. :frowning:

tell application "Finder" to set theFolders to selection as alias list

repeat with aFolder in theFolders
	set the contents of aFolder to quoted form of POSIX path of aFolder & " "
end repeat

set theImages to paragraphs of (do shell script "find " & theFolders & " -iname '*.jpg' | sort -R")

repeat with aFile in theImages
	set contents of aFile to POSIX file aFile
end repeat

tell application "Finder" to open theImages -- not included in timing tests

I reran my timing test with 1 selected folder containing 487 files. Script Geek reported 31 milliseconds. I then went down one level in that same folder and selected all 11 subfolders, which contained the same 487 files. Script Geek reported 46 milliseconds.

I modified this script to coerce the aFile variable to an alias and Script Geek reported 100 milliseconds with 1 folder selected and 487 files. So, alias coercion does appear to significantly impact execution speed.

Yes I just realized that opening all of those images in Preview is not a good idea. I actually used Preview as a place holder so in the future I can replace it with any other application.

Peavine,

I tested your last script and it works just fine. And about 2 times faster than mine.

Congratulations, your script is the most effective plain AppleScript version to perform the given task.

Thanks KniazidisR. I appreciate that.

Depending on the app, and what you’re doing with the files subsequently, you can open them asynchronously by modifying the code above like this:

set theJpegs to theJpegs's shuffledArray() -- no need to coerce to list, which takes time
--open them
current application's NSWorkspace's sharedWorkspace()'s openURLs:theJpegs withAppBundleIdentifier:"com.apple.finder" options:(current application's NSWorkspaceLaunchDefault) additionalEventParamDescriptor:(missing value) launchIdentifiers:(missing value)
-- or:
current application's NSWorkspace's sharedWorkspace()'s openURLs:theJpegs withApplicationAtURL:(path to application "Finder") options:(current application's NSWorkspaceLaunchDefault) configuration:(missing value) |error|:(missing value)