Detect when a User takes a Screenshot
Asked Answered
R

3

18

I am looking for way for my app to receive a notification when the user takes a screenshot either with Command-Shift-3 or Command-Shift-4.

An example of this are apps like Droplr and Cloud App which automatically upload the screenshot taken.

I have been searching around and found out that it may have something to do with Darwin Notifications but I am unsure as to where to start.

Rivarivage answered 23/12, 2010 at 8:1 Comment(3)
Take a look at this similar question: #2816002, which seems to have a good answerLucius
That is a good answer there and I will take a look. Thanks.Rivarivage
Just posted my answer to this: #4517352.Rivarivage
I
20

This was mentioned in one of the earlier comments, but you can use an NSMetadataQuery searching for files where kMDItemIsScreenCapture = 1. This is a special attribute that gets added to screenshot files.

I just whipped up a little demo showing how to do this and posted it on github:

https://github.com/davedelong/Demos/blob/master/ScreenShot%20Detector

Ilowell answered 16/4, 2011 at 6:57 Comment(10)
Haha. Wow that's much simpler than what I had done, thanks very much for doing this!Rivarivage
@Rivarivage yeah, actually finding the screenshots is pretty simple. As for the ui, bindings made it entirely codeless. :) just a couple of simple value transformers to work with the image view and the pathcontrol.Ilowell
The bindings were even more astonishing. I never thought bindings could be so powerful and save you so many lines of code!Rivarivage
@Rivarivage use them with caution. They're a royal pain to debug, and since they're specified in the xib, they can be easily disconnected and lost. But they are cool. :)Ilowell
@DaveDeLong When launching an app at startup, the NSMetadataQueryDidUpdateNotification doesn't fire for over a minute after launch. Any idea why? (It works flawlessly when launched manually)Greeting
@Greeting in the demo code I posted above, the update notification fires almost instantly after launch, so I don't know why's it's not for you. You're not blocking the main runloop, are you?Ilowell
@DaveDeLong, thank you for sharing the code with the community. Using your example project, I am trying to only get a notification 'only' when a screenshot image is created. Right now the code also detects a change if any such file is deleted and sends a notification, which sort of breaks the flow that I want in my cocoa app. any ideas?Cruce
I figured it out :).. Only added the following notification :[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryUpdated:) name:NSMetadataQueryDidUpdateNotification object:query];. and then in the notifcation method did this:: NSMetadataItem *item = [[notification.userInfo objectForKey:(NSString *)kMDQueryUpdateAddedItems] lastObject]; if (item) { NSString *screenShotPath = [item valueForAttribute:NSMetadataItemPathKey]; NSLog(screenShotPath); [self uploadScreenshotAtPath:screenShotPath]; }Cruce
This will not work if user had disabled spotlight process.Indene
just a note: an attacker can delete the extended attribute immediately after taking the screencapture and possibly be undetectable xattr -d com.apple.metadata:kMDItemIsScreenCapture ~/Desktop/myscreenshot.jpgSeedbed
R
4

This is how I have done it, it is a bit complicated but I will try and take you through it step by step:


Before we start, in your header file declare the following variables and methods :

BOOL shouldObserveDesktop;
NSDictionary *knownScreenshotsOnDesktop;
NSString *screenshotLocation;
NSString *screenshotFilenameSuffix;

- (void)startObservingDesktop;
- (void)stopObservingDesktop;
- (NSDictionary *)screenshotsOnDesktop;
- (NSDictionary *)screenshotsAtPath:(NSString *)dirpath modifiedAfterDate:(NSDate *)lmod;
- (void)checkForScreenshotsAtPath:(NSString *)dirpath;
- (NSDictionary *)findUnprocessedScreenshotsOnDesktop;

Now in your implementation file, firstly add this code:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    screenshotLocation = [[NSString stringWithString:@"~/Desktop"] retain];
    screenshotFilenameSuffix = [[NSString stringWithString:@".png"] retain];
    knownScreenshotsOnDesktop = [[self screenshotsOnDesktop] retain];
    [self startObservingDesktop];
}

This sets up the variables up for when all the methods are called. Next add:

- (void)onDirectoryNotification:(NSNotification *)n {
    id obj = [n object];
    if (obj && [obj isKindOfClass:[NSString class]]) {
        [self checkForScreenshotsAtPath:screenshotLocation];
    }
}

- (void)startObservingDesktop {
    if (shouldObserveDesktop)
        return;
    NSDistributedNotificationCenter *dnc = [NSDistributedNotificationCenter defaultCenter];
    [dnc addObserver:self selector:@selector(onDirectoryNotification:) name:@"com.apple.carbon.core.DirectoryNotification" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
    shouldObserveDesktop = YES;
}

- (void)stopObservingDesktop {
    if (!shouldObserveDesktop)
        return;
    NSDistributedNotificationCenter *dnc = [NSDistributedNotificationCenter defaultCenter];
    [dnc removeObserver:self name:@"com.apple.carbon.core.DirectoryNotification" object:nil];
    shouldObserveDesktop = NO;
}

Here we observe the notification that will be called when a screenshot is taken and pass it the method to call (in this case onDirectoryNotification:). There is also the method to stop observing the desktop/notification. The notification calls checkForScreenshotsAtPath: which will check for screenshots on the desktop. The following is the code for that method and the other methods that it calls:

-(void)checkForScreenshotsAtPath:(NSString *)dirpath {        
    NSDictionary *files;
    NSArray *paths;

    // find new screenshots
    if (!(files = [self findUnprocessedScreenshotsOnDesktop]))
        return;

    // sort on key (path)
    paths = [files keysSortedByValueUsingComparator:^(id a, id b) { return [b compare:a]; }];

    // process each file
    for (NSString *path in paths) {
        // Process the file at the path
    }
}

-(NSDictionary *)findUnprocessedScreenshotsOnDesktop {
    NSDictionary *currentFiles;
    NSMutableDictionary *files;
    NSMutableSet *newFilenames;

    currentFiles = [self screenshotsOnDesktop];
    files = nil;

    if ([currentFiles count]) {
        newFilenames = [NSMutableSet setWithArray:[currentFiles allKeys]];
        // filter: remove allready processed screenshots
        [newFilenames minusSet:[NSSet setWithArray:[knownScreenshotsOnDesktop allKeys]]];
        if ([newFilenames count]) {
            files = [NSMutableDictionary dictionaryWithCapacity:1];
            for (NSString *path in newFilenames) {
                [files setObject:[currentFiles objectForKey:path] forKey:path];
            }
        }
    }

    knownScreenshotsOnDesktop = currentFiles;
    return files;
}

-(NSDictionary *)screenshotsOnDesktop {
    NSDate *lmod = [NSDate dateWithTimeIntervalSinceNow:-5]; // max 5 sec old
    return [self screenshotsAtPath:screenshotLocation modifiedAfterDate:lmod];
}

They were the first 3 methods that the notification in turn calls and the following code is the final method screenshotsAtPath:modifiedAfterDate: which I will warn you is extremely long as it has to confirm that the file is definitely a screenshot:

-(NSDictionary *)screenshotsAtPath:(NSString *)dirpath modifiedAfterDate:(NSDate *)lmod {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSArray *direntries;
    NSMutableDictionary *files = [NSMutableDictionary dictionary];
    NSString *path;
    NSDate *mod;
    NSError *error;
    NSDictionary *attrs;

    dirpath = [dirpath stringByExpandingTildeInPath];

    direntries = [fm contentsOfDirectoryAtPath:dirpath error:&error];
    if (!direntries) {
        return nil;
    }

    for (NSString *fn in direntries) {

        // always skip dotfiles
        if ([fn hasPrefix:@"."]) {
            //[log debug:@"%s skipping: filename begins with a dot", _cmd];
            continue;
        }

        // skip any file not ending in screenshotFilenameSuffix (".png" by default)
        if (([fn length] < 10) ||
            // ".png" suffix is expected
            (![fn compare:screenshotFilenameSuffix options:NSCaseInsensitiveSearch range:NSMakeRange([fn length]-5, 4)] != NSOrderedSame)
            )
        {
            continue;
        }

        // build path
        path = [dirpath stringByAppendingPathComponent:fn];

        // Skip any file which name does not contain a space.
        // You want to avoid matching the filename against
        // all possible screenshot file name schemas (must be hundreds), we make the
        // assumption that all language formats have this in common: it contains at least one space.
        if ([fn rangeOfString:@" "].location == NSNotFound) {
            continue;
        }

        // query file attributes (rich stat)
        attrs = [fm attributesOfItemAtPath:path error:&error];
        if (!attrs) {
            continue;
        }

        // must be a regular file
        if ([attrs objectForKey:NSFileType] != NSFileTypeRegular) {
            continue;
        }

        // check last modified date
        mod = [attrs objectForKey:NSFileModificationDate];
        if (lmod && (!mod || [mod compare:lmod] == NSOrderedAscending)) {
            // file is too old
            continue;
        }

        // find key for NSFileExtendedAttributes
        NSString *xattrsKey = nil;
        for (NSString *k in [attrs keyEnumerator]) {
            if ([k isEqualToString:@"NSFileExtendedAttributes"]) {
                xattrsKey = k;
                break;
            }
        }
        if (!xattrsKey) {
            // no xattrs
            continue;
        }
        NSDictionary *xattrs = [attrs objectForKey:xattrsKey];
        if (!xattrs || ![xattrs objectForKey:@"com.apple.metadata:kMDItemIsScreenCapture"]) {
            continue;
        }

        // ok, let's use this file
        [files setObject:mod forKey:path];
    }

    return files;
}

Well, there you have it. That's how I was able to detect when the user takes a screenshot, it probably has a few bugs but it seems to work fine at the moment. If you want all the code in one here are the links for it at pastebin.com:

Header - http://pastebin.com/gBAbCBJB

Implementation - http://pastebin.com/VjQ6P3zQ

Rivarivage answered 23/12, 2010 at 13:33 Comment(6)
Wouldn't it be easier to just keep a NSMetadataQuery / MDQuery running to watch for new matches for "kMDItemIsScreenCapture == 1" ?Fatuitous
Hmm… Never though of that, I'll have a look. Thanks!Rivarivage
Also keep in mind that the user can change the location that screenshots are saved. It won't always be the desktop.Worl
Yes, I was planning on using this code pastebin.com/VQRgbjbc to get the location but it didn't seem to work for me as the com.apple.screencapture defaults did not exist.Rivarivage
NSDictionary *xattrs = [attrs objectForKey:@"NSFileExtendedAttributes"];Aceydeucy
With respect to @Pierre's comment, I whipped up a little demo app to run a NSMetadataQuery searching for kMDItemIsScreenCapture = 1 files: github.com/davedelong/Demos/tree/master/ScreenShot%20DetectorIlowell
C
-4

youll have to register an object to recieve the system notification when a user takes a screen shot

so:

[[NSNotificationCenter defaultCenter] addObserver: theObjectToRecieveTheNotification selector:@selector(theMethodToPerformWhenNotificationIsRecieved) name:@"theNameOftheScreenCapturedNotification" object: optionallyAnObjectOrArgumentThatIsPassedToTheMethodToBecalled];

not sure what the notification name is but it's probably out there.

don't forget to unregister yourself as well in dealloc:

[[NSNotificationCenter defaultCenter] removeObserver:self];

Chartres answered 23/12, 2010 at 8:11 Comment(2)
I already know how to do that but the problem is knowing what notification to observe. As far as I know there isn't one for when a user takes a screenshot.Rivarivage
I can't find such a notification either.Lucius

© 2022 - 2024 — McMap. All rights reserved.