image manipulation - extract portion / crop borders

When ImageEvents don’t suffice, I tend to use PHP for image manipulation.

Scenario: a folder with hundreds of image-sequences, all having unwanted borders of the same width.

My solution:

(*
expects a folder containing image sequence of jpegs
Cuts off pixels from the borders and replaces the original files

should you be cropping more than there is available
in any direction then that image remains untouched

requires PHP 5 or at least gd.lib installed

written 20070809 by Luke JZ aka SwissalpS http://bbs.applescript.net/profile.php?id=15718
Creative Commons Non Comercial Share Alike
*)

-- set these for your needs
-- iLeft, iTop, iRight, iBottom represent the amount of pixels you would like to remove
-- iQuality is a value from 0 to 100 where 0 makes the smallest files
set {iLeft, iTop, iRight, iBottom, iQuality, fileList, phpPath} to {50, 50, 30, 70, 100, makeFileList(choose folder), "/usr/local/php5/bin/php"}

-- the rest of this script doesn't need any changing

set phpScript to "$aFiles = array(" & filesListPHPready(fileList) & ");
foreach($aFiles as $sPathFile) {
	fCropAwayBorders($sPathFile, " & iLeft & ", " & iTop & ", " & iRight & ", " & iBottom & ", " & iQuality & ");
}

function fCropAwayBorders($sPathFile, $iLeft, $iTop, $iRight, $iBottom, $iQuality = 75) {
	if ((0 > $iQuality) || (100 < $iQuality)) { $iQuality = 75; }
	$rImgO = @imagecreatefromjpeg($sPathFile); // Attempt to open
	if (! $rImgO) { // See if it failed
		$rImgN = imagecreatetruecolor(384, 248); // Create a black image
		$iBackCol = imagecolorallocate($rImgN, 0, 0, 0);
		$iTextCol = imagecolorallocate($rImgN, 255, 255, 255);
		imagefilledrectangle($rImgN, 0, 0, 384, 248, $iBackCol);
		// Output an errmsg
		imagestring($rImgN, 1, 5, 5, 'Error loading ' . $sPathFile, $iTextCol);
	} else {
		$aSize = getimagesize($sPathFile);
		$iDestX = 0; $iDestY = 0; $iSrcX = $iLeft; $iSrcY = $iTop;
		$iSrcW = $aSize[0] - $iLeft - $iRight;
		$iSrcH = $aSize[1] - $iTop - $iBottom;
		$rImgN = imagecreatetruecolor($iSrcW, $iSrcH);
		imagecopy($rImgN, $rImgO, $iDestX, $iDestY, $iSrcX, $iSrcY, $iSrcW, $iSrcH);
	}
	$bSaveCheck = @imagejpeg($rImgN, $sPathFile, $iQuality);
}"

set shellScript to phpPath & " -r " & quoted form of phpScript
set theResult to do shell script shellScript

to filesListPHPready(fileList)
	set fileString to ""
	repeat with thisItem in fileList
		if "" ≠ fileString then
			set fileString to fileString & ", '" & (POSIX path of thisItem) & "'"
		else
			set fileString to fileString & "'" & (POSIX path of thisItem) & "'"
		end if
	end repeat
	return fileString
end filesListPHPready

to makeFileList(startFolder)
	tell application "Finder"
		set fileList to startFolder's files
		set outList to {}
		repeat with thisItem in fileList
			set outList's end to thisItem as alias
		end repeat
	end tell
	return outList
end makeFileList

I think this is a really neat application of php from AppleScript. Kudos for thinking of it.

Having said that, if you are using Tiger, this method of getting the list of files in a folder is 5-1/2 times faster than the method you use (but won’t work in Jaguar, as I recall):

to makeFileList2(startFolder) -- My usual approach with return statement removed.
	tell application "Finder" to try
		return files of entire contents of startFolder as alias list
	on error -- only one file (a bug in "as alias list")
		return files of entire contents of startFolder as alias as list
	end try
end makeFileList2

Thanks Adam, now it’s massively faster :smiley: getting ready. Considering my batches are 600+ images.
I added a filter to return only jpegs. This helps avoid possible php error.

(*
expects a folder containing image sequence of jpegs
Cuts off pixels from the borders and replaces the original files

should you be cropping more than there is available
in any direction then that image remains untouched

requires PHP 5 or at least gd.lib installed

written 20070809 by Luke JZ aka SwissalpS
http://bbs.applescript.net/profile.php?id=15718
Creative Commons Non Comercial Share Alike
This script has been posted on
http://bbs.applescript.net/viewtopic.php?id=22132
*)

-- set these for your needs
-- iLeft, iTop, iRight, iBottom represent the amount of pixels you would like to remove
-- iQuality is a value from 0 to 100 where 0 makes the smallest files
set {iLeft, iTop, iRight, iBottom, iQuality, fileList, phpPath} to {57, 86, 57, 18, 100, makeFileList(choose folder), "/usr/local/php5/bin/php"}

-- the rest of this script doesn't need any changing

beep 3 -- let us know that disk operation is starting
set phpScript to "$aFiles = array(" & filesListPHPready(fileList) & ");
foreach($aFiles as $sPathFile) {
	fCropAwayBorders($sPathFile, " & iLeft & ", " & iTop & ", " & iRight & ", " & iBottom & ", " & iQuality & ");
}

function fCropAwayBorders($sPathFile, $iLeft, $iTop, $iRight, $iBottom, $iQuality = 75) {
	if ((0 > $iQuality) || (100 < $iQuality)) { $iQuality = 75; }
	$rImgO = @imagecreatefromjpeg($sPathFile); // Attempt to open
	if (! $rImgO) { // See if it failed
		$rImgN = imagecreatetruecolor(384, 248); // Create a black image
		$iBackCol = imagecolorallocate($rImgN, 0, 0, 0);
		$iTextCol = imagecolorallocate($rImgN, 255, 255, 255);
		imagefilledrectangle($rImgN, 0, 0, 384, 248, $iBackCol);
		// Output an errmsg
		imagestring($rImgN, 1, 5, 5, 'Error loading ' . $sPathFile, $iTextCol);
	} else {
		$aSize = getimagesize($sPathFile);
		$iDestX = 0; $iDestY = 0; $iSrcX = $iLeft; $iSrcY = $iTop;
		$iSrcW = $aSize[0] - $iLeft - $iRight;
		$iSrcH = $aSize[1] - $iTop - $iBottom;
		$rImgN = imagecreatetruecolor($iSrcW, $iSrcH);
		imagecopy($rImgN, $rImgO, $iDestX, $iDestY, $iSrcX, $iSrcY, $iSrcW, $iSrcH);
	}
	$bSaveCheck = @imagejpeg($rImgN, $sPathFile, $iQuality);
}"

set shellScript to phpPath & " -r " & quoted form of phpScript
set theResult to do shell script shellScript
beep 4 -- signal that opperation has completed

to filesListPHPready(fileList)
	set fileString to ""
	repeat with thisItem in fileList
		if "" ≠ fileString then
			set fileString to fileString & ", '" & (POSIX path of thisItem) & "'"
		else
			set fileString to fileString & "'" & (POSIX path of thisItem) & "'"
		end if
	end repeat
	return fileString
end filesListPHPready

(* this was the old slow way of getting the files the slow way
this was the handler before Adam kindly supplied his fast
and bug considering routine. *)
to makeFileListJaguar(startFolder)
	tell application "Finder"
		set fileList to startFolder's files
		set outList to {}
		repeat with thisItem in fileList
			set outList's end to thisItem as alias
		end repeat
	end tell
	return outList
end makeFileListSlow

-- thanks to Adam Bell http://bbs.applescript.net/profile.php?id=9171
-- modified to filter out jpegs
-- possibly doesn't work on Tiger
to makeFileList(startFolder) -- My usual approach with return statement removed.
	tell application "Finder" to try
		return (files of entire contents of startFolder whose kind = "JPEG Image") as alias list
	on error -- only one file (a bug in "as alias list")
		return (files of entire contents of startFolder whose kind = "JPEG Image") as alias as list
	end try
end makeFileList

Actually I was hopping something like this would work:

return POSIX path of files of entire contents of a whose kind = "JPEG Image") as alias list

and then simply coerce the list for the php array:

set {atid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "', '"}
return "'" & fileList & "'" as Unicode text
set AppleScript's text item delimiters to atid

Hi Luke,

probably the fastest way to gather the files is to use Spotlight (of course Tiger only).
The result is a list of POSIX paths

to makeFileList(startFolder) 
	return paragraphs of (do shell script "mdfind -onlyin " & quoted form of POSIX path of startFolder & " 'kMDItemKind = \"*JPEG*\"'")
end makeFileList

That’s a great idea Stefan. Two questions came up while testing:

  • does this work on folders of drives that are not being indexed such as network drives? This would mean that mdfind causes live indexing.
  • is it really faster? My feeling says it’s about the same speed.

Thanks for the pointer, using spotlight to filter hasn’t crossed my mind yet, I’m not familiar with it’s syntax and behaviour yet.

No

Yes, mdfind is faster than the Finder using entire contents, especially for large contents

I’m looking forward to trying it myself later on. I overlooked it back in August. :slight_smile:

The ‘alias list’ approach has in fact worked since OS 8.6 or before, but ‘entire contents’ wasn’t totally reliable before OS X.

It turns out to be even faster to coerce the entire contents to Unicode text. Since the ultimate aim is to recoerce everything to POSIX paths, we could do it that way. The handler below makes the bold assumption that files whose kind is “JPEG Image” will have the name extension “.jpg” or “.jpeg” and that only such files will be so named. It’s probably not as fast as Stefan’s method, but it works at least as far back as Jaguar and on unindexed drives.

to filesListPHPready(startFolder)
	script o
		property pathList : missing value
	end script
	
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to return as Unicode text
	tell application "Finder" to set o's pathList to paragraphs of (entire contents of folder startFolder as Unicode text)
	repeat with i from 1 to (count o's pathList)
		set thisPath to item i of o's pathList
		if (thisPath ends with ".jpg") or (thisPath ends with ".jpeg") then
			set item i of o's pathList to quoted form of thisPath's POSIX path
		else
			set item i of o's pathList to missing value
		end if
	end repeat
	
	set AppleScript's text item delimiters to ", " as Unicode text
	set o's pathList to (o's pathList's every Unicode text) as Unicode text
	set AppleScript's text item delimiters to astid
	
	return o's pathList
end filesListPHPready

set startFolder to (choose folder)
filesListPHPready(startFolder)

Edit: Two cosmetic details in the above script.

I’ve now tried it on my Tiger machine and it doesn’t work. The shell script errors with the message: “sh: line 1: /usr/local/php5/bin/php: No such file or directory”.

If I change the value of phpPath to simply “php”, I get another error whose exact form depends on whether the php-ready file list was created with the original handlers or with mine.

With original: “Parse error: parse error, expecting `')” in Command line code on line 1"

With mine: "Warning: Unexpected character in input: '' (ASCII=92) state=1 in Command line code on line 1

Parse error: parse error, expecting `‘)’’ in Command line code on line 1"

The problem here is that my test folder contains a couple of files with apostrophes in their names. My handler uses ‘quoted form’ on each path whereas the original simply adds quotes, so the apostrophes are escaped in my version.

If I remove the two files with apostrophes, the shell script gives a new error ” “The command exited with a non-zero status.”

I would prefer to argue that the problem is that the filenames are not appropriately quoted when they get to PHP, not that they contain a certain character that happens to be used when quoting stuff in many languages. :wink:

quoted form of is only really good for the shell (or something else that works similarly). PHP single-quoting does not work like Bourne (normal, Bourne Again (bash), or even POSIX) shell single quoting. To get data into PHP one would need to pass it through the command line (being sure to use quoted form of there), or incorporate it into the PHP script itself, in which case it will need to be quoted according to PHP quoting rules (and if and only if the script text is being passed on the command line (via -r), then the it needs to be processed by quoted form of).

A quoting handler for PHP string literals follows. It is followed by some test/demonstration code. Note: I have no experience with PHP, I wrote it based only on the referenced web page:

to quoteForPHP(str)
	(* Reference: http://us3.php.net/manual/en/language.types.string.php#language.types.string.syntax.single
	 * Single-quoted strings in PHP require backslashed single-quotes.
	 * One may also backslash backslashes, but it is not always necessary.
	 * Other backslashes sequences (besides single-quote and backslash) are treated as literal (both the backslash and the following character)
	 *	 This appears to mean that all single backslashes need not be doubled, but all multiple-backslashes must be doubled (er, except the last one, if there is an odd number, since it will be treated like a single backslash.).
	 *	 It is probably algorithmically easier to just double all backslashes, whether single or multiple.
	 *)
	try
		set {otid, text item delimiters} to {text item delimiters, {"\\"}}
		set str to text items of str
		set text item delimiters to {"\\\\"}
		tell str to set str to beginning & ({""} & rest)
		set text item delimiters to {"'"}
		set str to text items of str
		set text item delimiters to {"\\'"}
		tell str to set str to beginning & ({""} & rest)
		set text item delimiters to otid
	on error
		set text item delimiters to otid
	end try
	"'" & str & "'" -- This seems to preserve the class of str (string/text/Unicode text) on 10.4.11 (AS 1.10.7)
end quoteForPHP

on run
	set strs to {"", " ", "'", "-e", "Time'll tell.", "some C-style escape that should never be interpreted by anyone: \\n\\r\\t\\a", "C:\\WINDEAUXS", "double\\\\backslash"}
	set allTrue to true
	repeat with s in strs
		(* Test shell quoting. *)
		set shellResult to do shell script "/bin/echo -n " & quoted form of s without altering line endings
		
		(* Test PHP quoting.
		 *	PHP-quote strings into PHP script.
		 *	shell-quote the script
		 *	pass the script on the command line
		 *)
		set phpScriptWithEmbeddedString to "$aStr = " & quoteForPHP(s) & ";" & return & "echo $aStr;"
		set phpEmbeddedResult to do shell script "/usr/bin/php -r " & quoted form of phpScriptWithEmbeddedString
		
		
		(* Alternative: pass the string(s) in via the PHP command line
		 *	shell-quote the PHP script; the strings are in PHP variables $argv[1] .. $argv[$argc-1]
		 *	shell-quote the strings
		 *	pass the script and string on the command line
		 *		Use "--" between the PHP arguments and the PHP script's arguments so PHP will not grab any arguments that start with a dash/hyphen
		 *)
		set phpScriptWithArgvString to "$aStr = $argv[1]; echo $aStr;"
		set phpArgvResult to do shell script "/usr/bin/php -r " & quoted form of phpScriptWithArgvString & " -- " & quoted form of s
		set contents of s to {orig:contents of s} & ¬
			{shell:{contents of s is shellResult, shellResult}} & ¬
			{phpEmbedded:{contents of s is phpEmbeddedResult, phpEmbeddedResult}} & ¬
			{phpArgv:{contents of s is phpArgvResult, phpArgvResult}} -- This peculiar syntax is just to get the linebreaks in there to try to make it a bit more readable.
		set allTrue to allTrue and ¬
			first item of shell of s and ¬
			first item of phpEmbedded of s and ¬
			first item of phpArgv of s
	end repeat
	{allTrue, strs}
end run

I get {true, .} on my system, so the test seem to indicate no failures in passing the tested string data back and forth for the demonstrated methods.

PS: Oh, and if you add some files with filenames that contain the return character, you will probably discover more problematic filenames (paragraphs of would break in inappropriate places). The newline character is another problem character.

Edits: Fix typo in text. Clarify my test results. Add PS.

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 3.0.4 (523.12)
Operating System: Mac OS X (10.4)

Hi, Chris.

Thanks for your insight. I’m pretty sure you’ve cracked the apostrophe problem and (were it to occur) the backslash one. It doesn’t make the original shell script work, but at least I’ve learned something. :slight_smile:

The only instances I know of returns in file names are with folders’ icon files. These are invisible and not returned by the Finder, so they shouldn’t be a problem here. I don’t know of any case where a line feed is allowed in a path name, so we might use (ASCII character 10) as the delimiter and get the text items instead of the paragraphs, though this appears to be a trifle slower.

I’ve incorporated your ideas into my handler, dealing with the apostrophes and backslashes in one go before splitting the text into individual paths. After the paths are converted to POSIX paths, the single quotes are included in the delimiter when the paths are coerced back to Unicode text. (The coercion’s done automatically with the concatenation near the end.)

to filesListPHPready(startFolder)
	script o
		property pathList : missing value
	end script
	
	-- Get the HFS paths of the folder's entire contents as a single, return-delimited text.
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to return as Unicode text
	tell application "Finder" to set pathText to entire contents of folder startFolder as Unicode text
	
	-- Convert any backslashes or apostrophes to PHP-compatible forms.
	considering case
		set AppleScript's text item delimiters to "\\" as Unicode text
		set pathText to pathText's text items
		set AppleScript's text item delimiters to "\\\\" as Unicode text
		set pathText to pathText as Unicode text
		set AppleScript's text item delimiters to "'" as Unicode text
		set pathText to pathText's text items
		set AppleScript's text item delimiters to "\\'" as Unicode text
		set pathText to pathText as Unicode text
	end considering
	
	-- Convert the individual paths to POSIX paths, losing any that don't relate to JPEGs.
	set o's pathList to pathText's paragraphs
	repeat with i from 1 to (count o's pathList)
		set thisPath to item i of o's pathList
		if (thisPath ends with ".jpg") or (thisPath ends with ".jpeg") then
			set item i of o's pathList to thisPath's POSIX path
		else
			set item i of o's pathList to missing value
		end if
	end repeat
	
	-- Coerce the POSIX paths to a single text, single-quoting and comma-separating them in the process.
	set AppleScript's text item delimiters to "', '" as Unicode text
	set pathText to ("'" as Unicode text) & (o's pathList's every Unicode text) & "'"
	set AppleScript's text item delimiters to astid
	
	return pathText
end filesListPHPready

set startFolder to (choose folder)
filesListPHPready(startFolder)

Hi Nigel,

I usually use ASCII character 0 and text items. :slight_smile:

I take my cues from my UNIX background where (on decent filesystems) the only characters that are not allowed to be part of the filename are the slash (because it is used as the separator between pathname components) and the “null” character (character 0 in most encodings, U+0000 in Unicode, etc.; because it terminates C-style strings which are used in the UNIX APIs that need filename or pathname parameters). Therefore, the null character is the only character that can be reliably used to separate pathnames in a “flat” byte stream (no data structures, quoting or special interpretation). It is not common to have filenames with returns or newlines in UNIX systems, but I have always tried to make my code robust enough to handle them anyway (after my early run-ins with programs that could not even handle spaces in filenames, usually due lazy coding in UNIX shell scripts).

So, while I agree that it is also not common to have LF (or CR, other than the “Icon\r” files) in filenames on Mac OS X systems, it is possible to create files with these characters (at least on my machine). I have been unable to do it via the Finder UI (Finder seems to reject the rename attempt when I paste in a CR or LF), but it is possible through Finder scripting(shown below) or through shell scripting (included for reference in the script below, but commented out; I use $‘\r’ and $‘\n’ in the script to get CR and LF).

set the clipboard to return
set the clipboard to (ASCII character 10)

path to desktop folder
set subjectFile to choose file with prompt "Pick a file to duplicate and rename with CR and LF in the filename" default location result
set cr to ASCII character 13
set lf to ASCII character 10
tell application "Finder"
	-- Get the info we need (extension, the name before the extension, and the containing folder)
	set {fullName, ext} to {name of subjectFile, name extension of subjectFile}
	if length of ext is not 0 then set ext to "." & ext
	set nameWithoutExt to text 1 through -((length of ext) + 1) of fullName
	set targetFolder to container of subjectFile
	
	-- Ensure the existence of files with CR and LF in the names
	set crFullName to nameWithoutExt & " CR>" & cr & "<CR demo" & ext
	if not (exists file crFullName of targetFolder) then
		set newF to duplicate f to targetFolder
		set name of newF to crFullName
	end if
	set lfFullName to nameWithoutExt & " LF>" & lf & "<LF demo" & ext
	if not (exists file lfFullName of targetFolder) then
		set newF to duplicate f to targetFolder
		set name of newF to lfFullName
	end if
	
	-- Collect the names of files with CR and LF (may be more than just the ones created/checked above if there were other files with names containing CR or LF)
	set crNames to name of items of targetFolder whose name contains cr
	set lfNames to name of items of targetFolder whose name contains lf
end tell

-- Report on the CR/LF content of the names, finding and noting the offset of each CR or LF character
{crNames:annotateWithCharOffetsOfEach(cr, crNames), lfNames:annotateWithCharOffetsOfEach(lf, lfNames)}

(*
-- Create and report on CR/LF files with the shell
"
FILE=" & quoted form of POSIX path of subjectFile & "

FULLNAME=\"$(basename \"$FILE\")\"
NAMEWITHOUTEXT=\"${FULLNAME%.*}\"
EXT=\"${FULLNAME#\"$NAMEWITHOUTEXT\"}\"
TARGETDIR=\"$(dirname \"$FILE\")\"

CRFULLNAME=\"$NAMEWITHOUTEXT\"$' CR>\\r<CR shell demo'\"$EXT\"
[ ! -e \"$CRFULLNAME\" ] && cp \"$FILE\" \"$TARGETDIR/$CRFULLNAME\"
LFFULLNAME=\"$NAMEWITHOUTEXT\"$' LF>\\n<LF shell demo'\"$EXT\"
[ ! -e \"$LFFULLNAME\" ] && cp \"$FILE\" \"$TARGETDIR/$LFFULLNAME\"

# The shell is not so good with string manipulations... Use Perl to report on the contents.
PERLSCRIPT='
use Data::Dumper;
$Data::Dumper::Terse=1;
$Data::Dumper::Indent=0;
$c=shift;
#for(@ARGV){
	print Data::Dumper->Dump(
			[offsetReport($c,@ARGV)],
			[q()]),
		qq(\\n);
#}
sub offsetReport{
	my $i, $r = [], $c = shift;
	for(@_){
		my $sr=[];
		while(m/$c/g){push @$sr,pos};
		push @$r,[$sr,$_]
	}
	@$r;
}
'
echo 'CRFILES:'
perl -e \"$PERLSCRIPT\" -- $'\\r' \"$TARGETDIR/\"*$'\\r'*
echo
echo 'LFFILES:'
perl -e \"$PERLSCRIPT\" -- $'\\n' \"$TARGETDIR/\"*$'\\n'*
"
do shell script result without altering line endings
*)

to annotateWithCharOffetsOfEach(c, l)
	set r to {}
	repeat with i in l
		set end of r to {getCharOffsets(c, contents of i), contents of i}
	end repeat
	r
end annotateWithCharOffetsOfEach

to getCharOffsets(c, s)
	set r to {}
	repeat with i from 1 to length of s
		if character i of s is c then set end of r to i
	end repeat
	r
end getCharOffsets

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 3.0.4 (523.12)
Operating System: Mac OS X (10.4)

OK, Chris. You’ve convinced me. :lol: