All widgets
Copy Text from Screen
Comes with Chopstick
Drag over any part of the screen to copy the text in it, read on-device.
Already installed? It's in your widget library.



What it runs
A widget is a folder of plain files. Here is all of its code.
widget.json
{
"id": "ocr",
"name": "Copy Text from Screen",
"icon": "icon.png",
"description": "Drag over any part of the screen to copy the text in it, read on-device.",
"anytime": false,
"initial": "selecting",
"states": {
"selecting": {
"task": true,
"on_enter": [ { "run": "read.sh", "then": "copied", "else": "failed" } ],
"compact": { "leading": { "symbol": "text.viewfinder" }, "trailing": "spinner" }
},
"copied": {
"task": true,
"open": true,
"dismiss_after": 5,
"on_enter": [ { "copy": "{{result.text}}" } ],
"compact": { "leading": { "symbol": "text.viewfinder", "tint": "green" }, "trailing": { "text": "Copied" } },
"expanded": {
"size": "regular",
"header": { "symbol": "text.viewfinder", "title": "Copied text" },
"blocks": [
{ "text": "{{result.text}}", "lines": 6 },
{ "button": { "symbol": "text.viewfinder", "label": "Select Again", "do": [ { "go": "selecting" } ] } }
]
}
},
"failed": {
"dismiss_after": 2,
"compact": { "leading": { "symbol": "text.viewfinder" }, "trailing": { "text": "{{error | Cancelled}}" } }
}
},
"preview": {
"states": [ "copied", "failed" ],
"data": { "result": { "text": "Quarterly report\nRevenue up 12% on last year" }, "error": "No text found" }
}
}
read.js
ObjC.import('Vision')
ObjC.import('stdlib')
// Prints the text in the image at argv[0] as {"text": ...}, line by line; exits 1 when there's none.
function run(argv) {
const handler = $.VNImageRequestHandler.alloc.initWithURLOptions($.NSURL.fileURLWithPath(argv[0]), $.NSDictionary.dictionary)
const request = $.VNRecognizeTextRequest.alloc.init
request.recognitionLevel = $.VNRequestTextRecognitionLevelAccurate
request.automaticallyDetectsLanguage = true
handler.performRequestsError($.NSArray.arrayWithObject(request), null)
const lines = []
const results = request.results
for (let i = 0; i < results.count; i++) {
const best = results.objectAtIndex(i).topCandidates(1).firstObject
if (best && !best.isNil()) lines.push(best.string.js)
}
if (!lines.length) {
console.log('No text found')
$.exit(1)
}
return JSON.stringify({ text: lines.join('\n') })
}
read.sh
# Drag over an area of the screen; prints its text as {"text": ...}. Esc exits 1.
shot="$CHOPSTICK_TMP/ocr.png"
rm -f "$shot"
screencapture -i -x "$shot"
[ -f "$shot" ] || exit 1
osascript -l JavaScript read.js "$shot"