Pixel Color Reading Script with shell script fails on Monterey

Thanks Mockman. The variable theColor returns the following, and I was uncertain how to convert this to RGB. I thought there might be some method that would do this but I couldn’t find anything. I’ll do some research on this.

BTW, all of the scripts in this thread including the OP’s return RGB 1, 119, 143 for my desktop, but System Preferences shows RGB 13, 99, 124, so something is amiss.

Colour is always irksome to deal with. A couple of things to think about…

What ‘y’ pixel are you working with? Perhaps one of them lies within a shadow of some sort. For example, if I use the ‘solid kelp’ background in the ‘solid colors’ folder, I get different colours when I change from y=30 to y=50, as 30 pixels down is within the menu bar’s shadow.

[format]-- {{79, 121, 110}} 30 px
– {{87, 135, 123}} 50 px[/format]

And, something else to consider: I have the numbers 13, 99, 124 in the ‘custom color…’ palette for the backgrounds. The profile I have selected is ‘generic RGB’ (selected using the palette’s gear icon. When I switch to ‘Color LCD Calibrated’, the numbers change to 16, 118, 141. Some of the other profiles really warp the numbers (like the Nikon NTSC one) but visually, the colour of my desktop doesn’t discernibly change. FWIW, the two closest seem to be the Apple RGB and the ITU BT.709 profiles but they’re all very different.

Finally, I noticed on stackoverflow, that there were a couple of questions around using some ObjC/NS commands to convert colours from trihex to RGB.

Mockman. I see what you mean. I rechecked the custom-color dialog. With “Generic RGB” the numbers are 13 99 124. With “Device RGB” the numbers are 1 118 143, which is almost exactly what my script returns. Anyways, my script returns exactly what the OP’s script returns, but without the errors, so the OP can decide how best to handle this.

I will do some additional research on how to convert trihex to RGB, which I would like to know. Thanks again.

Peavine, your first code snippet produces an empty list on my Mojave machine, and the second example results in the error message “missing value doesn’t understand the “redComponent” message.” I will attempt to mess around with this when I can more conveniently test it on Monterey.

What do you mean by trihex—values like #758c49? I simplified the original example code.


HEX2RGB(do shell script "thePath=" & ((path to desktop folder as text) & "test.bmp")'s POSIX path's quoted form & "; screencapture -mR '0,0,50,50' -t bmp $thePath; xxd -p -l 3 -s 54 $thePath | sed 's/\\(..\\)\\(..\\)\\(..\\)/\\3\\2\\1/' ")

on HEX2RGB(RGB_Hex)
	tell "0123456789" to set {decSys, hexSys} to {it, it & "ABCDEF"}
	set RGB to {}
	repeat with couplet in (RGB_Hex's {text 1 thru 2, text 3 thru 4, text 5 thru 6})
		set RGB's end to ((16 ^ 1) * ((offset of couplet's item 1 in hexSys) - 1)) + ((16 ^ 0) * ((offset of couplet's item 2 in hexSys) - 1))
	end repeat
	RGB
end HEX2RGB

Marc. Thanks for testing my scripts. The first script was a quick rewrite of a portion of the OP’s script, and I wrote it just to help identify the issue he was experiencing. I would probably ignore that script.

I retested my second script, which I thought might be a better way to accomplish what the OP wants, and it works fine for me. I’ll be interested to see what you find when testing it on a Monterey computer.

Trihex was a term I took from Mockman’s post, and I assumed without thinking that it was the technical term for the output of the colorAtX method (which clearly it’s not):

Edit: August 25, 2022
I just retested my first script and it doesn’t work for me either. I’ll delete that script. The second script works as expected.

While researching this topic, I happened upon a script by Shane at:

https://macscripter.net/viewtopic.php?pid=198596

Shane used the clipboard to store the screencapture, which seems a faster way to do this. I have no understanding of a few of the classes used by Shane, so I suggest this as a possible approach for refinement. Just in general, however, the script does appear to work.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set {x, y} to {50, 50} -- screencapture x and y coordinates
set {w, h} to {5, 2} -- screencapture width and height
do shell script "screencapture -R " & x & "," & y & "," & w & "," & h & " -c"
set theData to current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF)
set theCIImage to current application's CIImage's imageWithData:theData
set thisRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage
set screenColors to {}
repeat with i from 1 to h -- screen capture height
	repeat with j from 1 to w -- screen capture width
		set theColor to (thisRep's colorAtX:(j - 1) y:(i - 1))
		set theRed to (theColor's redComponent()) * 255
		set theGreen to (theColor's greenComponent()) * 255
		set theBlue to (theColor's blueComponent()) * 255
		set end of screenColors to {theRed as integer, theGreen as integer, theBlue as integer}
	end repeat
end repeat
return screenColors

Sorry… I just made it up on the spot, so as to make the distinction between the three pairs of hex numbers. I guess you could also think of it as a 3-digit base256 number. By the way, the colorAtX output is just the color value expressed on a scale of 0 to 1.

@ peavine,

I followed all the dialog with great interest - although the HEX conversion was an unnecessary detour as the HEX2RGB handler seem to be accurate if a bit ornate.
However, your second script makes a desktop folder when it runs. This is undesirable for the functionality of the entire large script. Did it, at least, work properly on Monterey? That wasn’t clear.

Of more interest is Shane’s script. Does this work reliably on Monterey, or do I have to test it? If so, I need to include it in a handler similar to mine as a complete test. I am assuming the output of the shell script is always some sort of HEX. As far as what I need, the EXACT content of RGB is not critical as long as it is consistent. In fact, perhaps it is possible for me to use only the HEX output because all I need to do is to match a color previously sampled the same way.

re:
use framework “AppKit”
use framework “Foundation”
use framework “QuartzCore”
use scripting additions

I do not completely understand these references except that they seem to add elements to vanilla AppleScript. Would this only be needed inside the handler, or does this need to precede the whole script?

Your thoughts?

TheKrell. I’ve included below a suggestion in the form of a handler. It returns a list of lists, and each item in the individual lists consist of:

  • the x point
  • the y point
  • the red value
  • the green value
  • the blue value

I did some limited testing, and the RGB colors returned by my script were reliable. The four statements you mention have to be at the top of the script, and, with rare exception, they should not interfere with the remainder of your script.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set xCoordinates to {100, 300, 500}
set yCoordinates to {200, 400, 600}

set rgbData to getRGBData(xCoordinates, yCoordinates)

-- just for testing purposes
repeat with anItem in rgbData
	display dialog "x: " & item 1 of anItem & linefeed & "y: " & item 2 of anItem & linefeed & "Red: " & item 3 of anItem & linefeed & "Green: " & item 4 of anItem & linefeed & "Blue: " & item 5 of anItem
end repeat

on getRGBData(xCoordinates, yCoordinates)
	set screenColors to {}
	repeat with yPoint in yCoordinates
		set yPoint to contents of yPoint
		repeat with xPoint in xCoordinates
			set xPoint to contents of xPoint
			do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
			set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
			set theCIImage to (current application's CIImage's imageWithData:theData)
			set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
			set theColor to (thisRep's colorAtX:0 y:0)
			set theRed to ((theColor's redComponent()) * 255) as integer
			set theGreen to ((theColor's greenComponent()) * 255) as integer
			set theBlue to ((theColor's blueComponent()) * 255) as integer
			set end of screenColors to {xPoint, yPoint, theRed, theGreen, theBlue}
		end repeat
	end repeat
	return screenColors
end getRGBData

A user may need to get the RGB values of specific screen coordinates, and, FWIW, I modified my script to accomplish that.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set theCoordinates to {{10, 16}, {26, 16}, {10, 100}} -- x and y screen coordinates
set rgbData to getRGBData(theCoordinates)

-- a summary dialog for testing purposes
set dialogText to {}
repeat with aList in rgbData
	set end of dialogText to "x " & item 1 of aList & " - y " & item 2 of aList & " - Red " & item 3 of aList & " - Green " & item 4 of aList & " - Blue " & item 5 of aList & linefeed
end repeat
display dialog dialogText as text buttons {"OK"} cancel button 1 default button 1

on getRGBData(theCoordinates)
	set screenColors to {}
	repeat with aList in theCoordinates
		set {xPoint, yPoint} to {item 1 of aList, item 2 of aList}
		do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
		set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
		set theCIImage to (current application's CIImage's imageWithData:theData)
		set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
		set theColor to (thisRep's colorAtX:0 y:0)
		set theRed to ((theColor's redComponent()) * 255) as integer
		set theGreen to ((theColor's greenComponent()) * 255) as integer
		set theBlue to ((theColor's blueComponent()) * 255) as integer
		set end of screenColors to {xPoint, yPoint, theRed, theGreen, theBlue}
	end repeat
	return screenColors
end getRGBData

@ peavine,
I started testing on Catalina first planning to move on to Monterey if it all works.
I modified your last script to do just one coordinate as so:

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set theCoordinates to {{10, 16}} -- x and y screen coordinates
set rgbData to getRGBData(theCoordinates)

-- a summary dialog for testing purposes
set dialogText to {}
repeat with aList in rgbData
	set end of dialogText to item 1 of aList & "  " & item 2 of aList & "  " & item 3 of aList & " "
end repeat
display dialog dialogText as text buttons {"OK"} cancel button 1 default button 1

on getRGBData(theCoordinates)
	set screenColors to {}
	repeat with aList in theCoordinates
		set {xPoint, yPoint} to {item 1 of aList, item 2 of aList}
		do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
		set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
		set theCIImage to (current application's CIImage's imageWithData:theData)
		set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
		set theColor to (thisRep's colorAtX:0 y:0)
		set theRed to ((theColor's redComponent()) * 255) as integer
		set theGreen to ((theColor's greenComponent()) * 255) as integer
		set theBlue to ((theColor's blueComponent()) * 255) as integer
		set end of screenColors to {theRed, theGreen, theBlue}
	end repeat
	return screenColors
end getRGBData

This worked just fine.

Then I tried using the one coordinate form in 2 repeat loops as so:

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set yrow to {327, 371, 415, 459, 503, 547}
set xrow to {2589, 2633, 2677, 2721, 2765}

repeat with ic from 1 to 6
	repeat with i from 1 to 5
		
		set X to item i of xrow
		set Y to item ic of yrow
		set theCoordinates to {{X, Y}} -- x and y screen coordinates
		set rgbData to getRGBData(theCoordinates)
		
		-- a summary dialog for testing purposes
		set dialogText to {}
		repeat with aList in rgbData
			set end of dialogText to item 1 of aList & "  " & item 2 of aList & "  " & item 3 of aList & " "
		end repeat
		display dialog dialogText as text buttons {"OK"} cancel button 1 default button 1
		
	end repeat
end repeat


on getRGBData(theCoordinates)
	set screenColors to {}
	repeat with aList in theCoordinates
		set {xPoint, yPoint} to {item 1 of aList, item 2 of aList}
		do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
		set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
		set theCIImage to (current application's CIImage's imageWithData:theData)
		set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
		set theColor to (thisRep's colorAtX:0 Y:0)
		set theRed to ((theColor's redComponent()) * 255) as integer
		set theGreen to ((theColor's greenComponent()) * 255) as integer
		set theBlue to ((theColor's blueComponent()) * 255) as integer
		set end of screenColors to {theRed, theGreen, theBlue}
	end repeat
	return screenColors
end getRGBData

This returns the error at runtime:
error “-[NSBitmapImageRep colorAtX:Y:]: unrecognized selector sent to instance 0x600002664660” number -10000
It fails at the first call of handler getRGBData.
Normally I can debug plain vanilla AppleScript but I do not understand Shane’s coding. Why does it go wrong at the second modification?

Whoops… I changed your first one as so:

set X to 10
set Y to 16

set theCoordinates to {{X, Y}} -- x and y screen coordinates

It failed the same way. It isn’t the loop. This handler seems to fail unless the inputs are literals. What am I missing?

TheKrell. I edited your script to address a number of issues, which included an unnecessary repeat loop and two variable names that caused a conflict. I tested this on my Monterey computer without issue.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

set yrow to {10, 100}
set xrow to {10, 25}
-- set yrow to {327, 371, 415, 459, 503, 547}
-- set xrow to {2589, 2633, 2677, 2721, 2765}

set rgbData to {}
repeat with ic from 1 to (count yrow)
	repeat with i from 1 to (count xrow)
		set xPoint to item i of xrow
		set yPoint to item ic of yrow
		set end of rgbData to getRGBData(xPoint, yPoint)
	end repeat
end repeat

-- a summary dialog for testing purposes
set dialogText to {}
repeat with aList in rgbData
	set end of dialogText to item 1 of aList & " " & item 2 of aList & " " & item 3 of aList & " "
end repeat
display dialog dialogText as text buttons {"OK"} cancel button 1 default button 1

on getRGBData(xPoint, yPoint)
	do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
	set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
	set theCIImage to (current application's CIImage's imageWithData:theData)
	set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
	set theColor to (thisRep's colorAtX:0 y:0)
	set theRed to ((theColor's redComponent()) * 255) as integer
	set theGreen to ((theColor's greenComponent()) * 255) as integer
	set theBlue to ((theColor's blueComponent()) * 255) as integer
	return {theRed, theGreen, theBlue}
end getRGBData

KniazidisR. I suspect that change was actually made by Script Editor or Script Debugger. To verify this, copy the OP’s second script in post 14 into Script Debugger, change Y to y, and then compile the script. You will note that y has been changed to Y.

Yes, this change makes the compilation. My apologies.

The script in post 15 should meet the OP’s needs, and it seems a good solution.

FWIW, I wrote a script that utilizes GUI scripting with the Digital Color Meter. I suspect this will only work with Monterey, and the script is clunky and slow but does return correct results. For this script to work, all items in Digital Color Meter’s View menu have to be unchecked. There are many other settings and these should be set to whatever is desired (although “Display in sRGB” is probably best). The returned values are text but are easily coerced to integers where needed.

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set xCoordinates to {10, 27}
	set yCoordinates to {15, 100}
	set rgbData to {}
	tell application "Digital Color Meter" to activate
	repeat with aYCoordinate in yCoordinates
		set aYCoordinate to contents of aYCoordinate
		repeat with anXCoordinate in xCoordinates
			set anXCoordinate to contents of anXCoordinate
			moveCursor(anXCoordinate, aYCoordinate)
			set end of rgbData to getRGBData()
		end repeat
	end repeat
	set rgbData to parseRGBData(rgbData) --> {{"218", "218", "218"}, {"32", "32", "32"}, {"2", "110", "132"}, {"255", "255", "255"}}
end main

on moveCursor(xCoordinate, yCoordinate)
	set cursorPoint to current application's NSMakePoint(xCoordinate, yCoordinate)
	current application's CGWarpMouseCursorPosition(cursorPoint)
	delay 0.5 -- test different values
end moveCursor

on getRGBData()
	tell application "System Events" to tell process "Digital Color Meter"
		click menu item "Copy Color as Text" of menu "Color" of menu bar 1
	end tell
	delay 0.5 -- test different values
	return the clipboard
end getRGBData

on parseRGBData(rgbData)
	set ATID to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {space, tab}
	repeat with anItem in rgbData
		set contents of anItem to text items 1 thru -2 of anItem -- a list
		-- set contents of anItem to (text items 1 thru -2 of anItem) as text -- a string
	end repeat
	set AppleScript's text item delimiters to ATID
	return rgbData
end parseRGBData

main()

@ peavine,
Yes, the script in post 15 does the job. I tested this modified version - closer to what I need to integrate into my final version, It works in Monterey.

Would you explain to me why compiling some of the other ones would change that small ‘y’ to a capital “Y”? I never noticed when that happened and assumed my lack of knowledge in peripheral coding caused it to fail at that point. Once it happened, subsequent copies contained the changed y letter causing run failure even if I may have done the right thing afterwards.

Now, I have to see if using this in my main script disrupts something else.

Anyhow, this whole process got me to the right place, and I thank you and all other’s concerned.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions


(*
set yrow to {10, 100}
set xrow to {10, 25}
*)


set yrow to {327, 371, 415, 459, 503, 547}
set xrow to {2589, 2633, 2677, 2721, 2765}


repeat with ic from 1 to 6
	repeat with i from 1 to 5
		set rgbData to {}
		set xPoint to item i of xrow
		set yPoint to item ic of yrow
		set cvec to getRGBData(xPoint, yPoint)
		
		display dialog ((((((ic as text) & " " & i as text) & " 
		" & xPoint as text) & " " & yPoint as text) & "
		" & item 1 of cvec as text) & " " & item 2 of cvec as text) & " " & item 3 of cvec as text
		
		(*
set end of rgbData to getRGBData(xPoint, yPoint)
		
		
		-- a summary dialog for testing purposes
		set dialogText to {}
		repeat with aList in rgbData
			set end of dialogText to item 1 of aList & " " & item 2 of aList & " " & item 3 of aList & " "
		end repeat
		display dialog ((i as text) & " " & ic as text) & "
		" & dialogText as text
*)
	end repeat
end repeat

on getRGBData(xPoint, yPoint)
	do shell script "screencapture -R " & xPoint & "," & yPoint & ",1,1 -c"
	set theData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
	set theCIImage to (current application's CIImage's imageWithData:theData)
	set thisRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theCIImage)
	set theColor to (thisRep's colorAtX:0 y:0)
	set theRed to ((theColor's redComponent()) * 255) as integer
	set theGreen to ((theColor's greenComponent()) * 255) as integer
	set theBlue to ((theColor's blueComponent()) * 255) as integer
	return {theRed, theGreen, theBlue}
end getRGBData

TheKrell. I’m glad that script did the job. :slight_smile:

As regards your question, I’m not certain but I believe Script Debugger or Script Editor sees the y in “colorAtX:0 y:0” and assuming it to be the variable Y (which is pointed to by theCoordinates handler parameter) changes y to Y. This can be seen by compiling the following:

set A to 1
set B to A
theHandler(B)
on theHandler(B)
	a -- will be changed to A when script compiled
end theHandler

However, the copy command is supposed to create an independend copy but that doesn’t seem to work in this instance, so my explanation may not be correct:

set A to 1
copy A to B
theHandler(B)
on theHandler(B)
	a -- will be changed to A when script compiled
end theHandler

Yes. It’s the way the AppleScript compiler works. The first appearances of the labels x and x are the upper-case ones in the lines set X to item i of xrow and set Y to item ic of yrow, so all subsequent occurrences of those labels are compiled in the same case. The obvious cures here are to change the first appearances to lower case or to use different labels. “Different labels” could include using bars with the upper-case versions:

set |X| to item i of xrow
set |Y| to item ic of yrow

set theColor to (thisRep's colorAtX:0 y:0)

Thanks Nigel for the explanation. :cool:

This topic is resolved but I thought I would add a final comment on accuracy.

I opened the Digital Color Meter and set it to the smallest aperture size–which the documentation appears to indicate is 1 pixel–and I set the color space to “Display in Generic RGB”. I then modified my script to return cursor position and generic RGB colors at the cursor position. Finally, I ran tests comparing the two.

With solid colors the returned results were essentially identical. However, on images with variegated colors, there were differences but they were generally quite small. For example, on an image of a grass lawn, Digital Color Meter returned 151 175 39 and my script returned 152 176 42.

Just as an aside, the script as writen returns the RGB data for one pixel; this can be changed by modifying theWidth and theHeight values. Also, if sRGB output is desired, replace genericRGBColorSpace in the script with sRGBColorSpace.

use framework "AppKit"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

on main()
	set {xCoordinate, yCoordinate} to getCursorPosition()
	set {redColor, greenColor, blueColor} to getRGBData(xCoordinate, yCoordinate)
	set dialogText to "X Coordinate: " & xCoordinate & linefeed & "Y Coordinate: " & yCoordinate & linefeed & linefeed & "Red Color: " & redColor & linefeed & "Green Color: " & greenColor & linefeed & "Blue Color: " & blueColor
	display dialog dialogText buttons {"OK"} default button 1 with title "Coordinates and Color at Cursor"
end main

on getCursorPosition()
	set cursorPosition to current application's NSEvent's mouseLocation()
	set {{x1, y1}, {x2, y2}} to (current application's NSScreen's mainScreen())'s frame()
	set cursorPosition to {(cursorPosition's x as integer), (y2 as integer) - (cursorPosition's y as integer)}
end getCursorPosition

on getRGBData(theX, theY) -- a minor rewrite of a handler by Shane Stanley
	set {theWidth, theHeight} to {1, 1}
	set theX to theX - (theWidth div 2) -- center width and height on cursor
	set theY to theY - (theHeight div 2) -- center width and height on cursor
	do shell script "screencapture -R " & theX & "," & theY & "," & theWidth & "," & theHeight & " -c"
	set imageData to (current application's NSPasteboard's generalPasteboard()'s dataForType:(current application's NSPasteboardTypeTIFF))
	set theImage to (current application's CIImage's imageWithData:imageData)
	set theFilter to current application's CIFilter's filterWithName:"CIAreaAverage"
	theFilter's setValue:theImage forKey:(current application's kCIInputImageKey)
	set theResult to theFilter's valueForKey:(current application's kCIOutputImageKey)
	set imageRep to (current application's NSBitmapImageRep's alloc()'s initWithCIImage:theResult)
	set theColors to (imageRep's colorAtX:0 y:0)
	set theColors to theColors's colorUsingColorSpace:(current application's NSColorSpace's genericRGBColorSpace())
	set redColor to ((theColors's redComponent()) * 255) as integer
	set greenColor to ((theColors's greenComponent()) * 255) as integer
	set blueColor to ((theColors's blueComponent()) * 255) as integer
	return {redColor, greenColor, blueColor}
end getRGBData

main()