Closed Saba-Sabato closed 7 months ago
You need runloop to wait for the completion handler to be called.
Also don't call dealloc
methods, that will result in hard crashes. That's similar to why you don't need to call __del__
in Python, although you won't get hard crashes when you do so.
@Saba-Sabato I am also learning pyobjc and trying to grab a screenshot with ScreenCaptureKit
. Did you ever get this working / have a working code example you can share?
Thanks! :)
Hi @ronaldoussoren, not sure if this is the right place to ask such a question (feel free to redirect) but could you provide some more context (or a reference) on how to properly wait for the completion handler to be called?
Like the original reporter, I'm trying to write a simple synchronous function that returns (or raises) after screen capture.
I tried some simple implementations with AppHelper.runConsoleEventLoop()
, Foundation.CFRunLoopRun()
, and Python's threading.Event
but couldn't get them to work well/robustly (e.g. terminating program, hanging if there is an error, etc).
Thanks, appreciate all your work on this!
Basic example:
import logging
def capture_screen(save_path: str):
from ScreenCaptureKit import (
SCContentFilter, # type: ignore
SCScreenshotManager, # type: ignore
SCStreamConfiguration, # type: ignore
SCShareableContent, # type: ignore
)
from AppKit import NSBitmapImageFileTypePNG, NSBitmapImageRep # type: ignore
import Quartz
import time
def shareable_content_completion_handler(shareable_content, error):
try:
if error:
logging.error(f"Shareable content completion handler error: {error}")
return
if not shareable_content:
logging.error("No shareable content in completion handler.")
return
if not shareable_content.displays():
logging.error("No displays in shareable content in completion handler.")
return
display = shareable_content.displays()[0]
content_filter = SCContentFilter.alloc().initWithDisplay_excludingApplications_exceptingWindows_(
display,
[],
[],
)
configuration = SCStreamConfiguration.alloc().init()
SCScreenshotManager.captureImageWithFilter_configuration_completionHandler_(
content_filter, configuration, capture_image_completion_handler
)
except Exception as e:
logging.exception(f"Exception in shareable_content_completion_handler: {e}")
def capture_image_completion_handler(image, error):
try:
if error:
logging.error(f"Capture image completion handler error: {error}")
return
if not image:
logging.error("No image in completion handler.")
return
bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage_(image)
if not bitmap_rep:
logging.error("Failed to create bitmap image representation.")
return
png_data = bitmap_rep.representationUsingType_properties_(
NSBitmapImageFileTypePNG, None
)
if not png_data:
logging.error("Failed to convert image to PNG format.")
return
png_data.writeToFile_atomically_(save_path, True)
logging.info(f"Screenshot saved as {save_path}.")
except Exception as e:
logging.exception(f"Exception in capture_image_completion_handler: {e}")
SCShareableContent.getShareableContentWithCompletionHandler_(
shareable_content_completion_handler
)
time.sleep(1) # TODO: replace with more robust approach
def main():
logging.basicConfig(level=logging.INFO)
capture_screen(
save_path="screenshot.png",
)
print("Screenshot captured.")
with open("screenshot.png", "rb") as f:
print(f"Screenshot size: {len(f.read())}")
main()
@torablien Hey, no, unfortunately I couldn't get ScreenCaptureKit to work. I painstakingly collected information scattered throughout the whole damn web just to get this simple example to work, after which I dropped the subject. I eventually went with another approach, here is some sample code to get you started:
from pathlib import Path
import Foundation
import Quartz
import UniformTypeIdentifiers
import objc
def write_screenshot_to_file(path: Path | str):
screenshot = get_screenshot()
url = convert_path_to_ns_url(path)
result = write_image_to_file(screenshot, url)
assert result is True, "Failed writing screenshot to file"
return
def get_screenshot():
return Quartz.CGDisplayCreateImage(Quartz.CGMainDisplayID())
def convert_path_to_ns_url(path: Path | str):
if isinstance(path, str):
path = Path(path)
url = Foundation.NSURL.fileURLWithPath_(str(path.absolute()))
return url
def write_image_to_file(image, path):
dest = Quartz.CGImageDestinationCreateWithURL(
path, UniformTypeIdentifiers.UTTypeJPEG.identifier(), 1, None
)
assert dest is not None, "Error creating destination, check parameters?"
Quartz.CGImageDestinationAddImage(dest, image, None)
result = Quartz.CGImageDestinationFinalize(dest)
return result
def main(path: Path | str):
with objc.autorelease_pool():
write_screenshot_to_file(path)
if __name__ == "__main__":
main(Path.cwd() / "screenshot.jpeg")
Good luck!
Hello, This is the first time I've used PyObjC. I'm trying to grab a screenshot in Python, using
ScreenCaptureKit
, adapting the code from this Apple Videos guide. (go tocode
tab, bottom two code blocks) I've encountered several problems along the way, and I'm here to ask if I've just gone about this completely the wrong way. I went through the PyObjC documentation and the relevant Apple documentation pages: ScreenCaptureKit, captureImageWithFilter, SCContentFilter, SCShareableContent. Here is the code I have so far:I can't figure out how to write normal, synchronous code to get the screenshot. The function
captureImageWithFilter
has a required parameterSCContentFilter
, which is initialized with a SCDisplay. To get that I need to call getShareableContentWithCompletionHandler which takes as a parameter a completion handler. That handler will receive the resultingSCDisplay
. Only then can I create the parameters required to callcaptureImageWithFilter
which in turn receives yet another handler, and only that final handler will have theCGImage
. This feels like callback hell, and I'm sure it's not the way it's supposed to be called. What is the correct way to do this?Thank you very very much, Saba