1

I try to get the screenshot from all monitors connected with my MAC to one picture. I know, how I can do this if every monitor's screenshot will saved to different pictures, but it is not what I want. I found function CGGetDisplaysWithRect, but my solution don't work, because output picture is empty. I expect, that problem with function CGDisplayCreateImageForRect (*displays, rect), because first parameter must be CGDirectDisplayID type, but not CGDirectDisplayID*. But I can't find function, which can create one picture with some CGDirectDisplayID objects.
Help me please!!!

#include <stdio.h>
#include <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
    CGDisplayCount displayCount;
    CGDirectDisplayID displays[32];
    memset(&displays, 0, 32);
    CGImageRef image[32];
    CGRect rect = CGRectNull;

    //grab the active displays
    if (CGGetActiveDisplayList(32, displays, &displayCount) != kCGErrorSuccess)
    {
        printf("Error occured: %s\n", strerror(errno));
    }

    //go through the list
    for (int i = 0; i < displayCount; i++)
    {
        if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)
        {
            continue;
        }
        //return the smallest rectangle wich contain the two source rectangles
        rect = CGRectUnion(rect, CGDisplayBounds(displays[i]));
        if (CGRectIsNull(rect))
        {
            printf("Error: %s", strerror(errno));
        }
    }

    CGFloat whitePoint[3];
    CGFloat blackPoint[3];
    CGFloat gamma[3];
    CGFloat matrix[9];

    CGColorSpaceRef colorSpace = CGColorSpaceCreateCalibratedRGB (&whitePoint[3], &blackPoint[3], &gamma[3], &matrix[9] );
    if(colorSpace == NULL)
    {
        printf("Error: %s", strerror(errno));
    }

    //CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);

    //Create bmp context for image
    CGContextRef context = CGBitmapContextCreate(NULL,                      //data
                                          CGRectGetWidth(rect),         //width
                                          CGRectGetHeight(rect),        //height
                                          8,                            //bitPerComponent, for RGB must be 8
                                          0,                            //if data == NULL, it must be 0
                                          colorSpace,                   //colorspace device independent
                                          kCGBitmapByteOrderDefault );  //bitmap info
    if(context == NULL)
    {
        printf("Error: %s", strerror(errno));
    }

    //Create a snapshot image
    for (int i = 0; i < displayCount; i++)
    {
        image[i] = CGBitmapContextCreateImage(context);
        if(image == NULL)
        {
            //printf("Error: %s", strerror(errno));
        }
    }

    //Create destination to image
    CFURLRef url = CFURLCreateWithString ( kCFAllocatorDefault, CFSTR("out.bmp"), NULL);
    if(url == NULL)
    {
        printf("Error: %s", strerror(errno));
    }
    CFErrorRef *error = NULL;
    CFURLRef urlToFile = CFURLCreateFilePathURL ( kCFAllocatorDefault, url, error );
    if(urlToFile == NULL)
    {
        //printf("Error: %s", error);
    }

    CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL(urlToFile, kUTTypeBMP, displayCount, NULL);
    if(imageDestination == NULL)
    {
        printf("Error: %s", strerror(errno));
    }

    //CGImageDestinationAddImage(imageDestination, image, NULL);
    CGImageDestinationFinalize(imageDestination);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    CFRelease(imageDestination);
    return 0;
} 

APDATE: I tried smth that me told below, but now I get error:

Error: CGBitmapContextCreate: unsupported parameter combination: 8 integer bits/component; 24 bits/pixel; 3-component color space; kCGImageAlphaNone; 3456 bytes/row.

neo
  • 197
  • 2
  • 14
  • Really??? Nobody can helps me??? – neo Jan 30 '15 at 09:19
  • The functions will likely help you if you check the return values like I suggested. – Mark Setchell Jan 30 '15 at 09:31
  • 1
    @MarkSetchell, malloc works correctly, I checked it yesterday. – neo Jan 30 '15 at 09:59
  • 1
    In your code example, you didn't initialize `rect`. Do you want an image of the entirety of all of the displays or do you want the image within a particular rectangle that may span multiple displays? Also, if you're just going to write the image to file, it's better to create your `CGImageDestination` with a URL. – Ken Thomases Jan 30 '15 at 10:11
  • 1
    @KenThomases, Thank you for your answer! If solution is exist, I want an image of the entirety of all of the displays, but, I couldn't find it. Because, I try to get an image within a particular rectangle that may span multiple displays....((( What function can calculate a rectangle, that contains all displays??? – neo Jan 30 '15 at 12:22

1 Answers1

5

Here's some code that should do it. On the one hand, I wasn't able to test on a multi-monitor system yet, but, on the other, the code was written without any assumptions about which display to use or where it is positioned. So, it should work.

    CGDirectDisplayID displays[32];
    uint32_t count;
    if (CGGetActiveDisplayList(sizeof(displays)/sizeof(displays[0]), displays, &count) != kCGErrorSuccess)
    {
        NSLog(@"failed to get display list");
        exit(EXIT_FAILURE);
    }

    CGRect rect = CGRectNull;
    CGRect primaryDisplayRect = CGRectZero;
    for (uint32_t i = 0; i < count; i++)
    {
        // if display is secondary mirror of another display, skip it
        if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)
            continue;

        CGRect displayRect = CGDisplayBounds(displays[i]);
        if (i == 0)
            primaryDisplayRect = displayRect;
        displayRect.origin.y = CGRectGetMaxY(primaryDisplayRect) - CGRectGetMaxY(displayRect);
        rect = CGRectUnion(rect, displayRect);
    }

    NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                                                                         pixelsWide:CGRectGetWidth(rect)
                                                                         pixelsHigh:CGRectGetHeight(rect)
                                                                      bitsPerSample:8
                                                                    samplesPerPixel:4
                                                                           hasAlpha:YES
                                                                           isPlanar:NO
                                                                     colorSpaceName:NSCalibratedRGBColorSpace
                                                                       bitmapFormat:0
                                                                        bytesPerRow:0
                                                                       bitsPerPixel:32];
    if (!imageRep)
    {
        NSLog(@"failed to create bitmap image rep");
        exit(EXIT_FAILURE);
    }

    NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithBitmapImageRep:imageRep];
    if (!context)
    {
        NSLog(@"failed to create graphics context");
        exit(EXIT_FAILURE);
    }

    [NSGraphicsContext saveGraphicsState];
    {
        [NSGraphicsContext setCurrentContext:context];
        CGContextRef cgcontext = [context graphicsPort];

        CGContextClearRect(cgcontext, CGRectMake(0, 0, CGRectGetWidth(rect), CGRectGetHeight(rect)));

        for (uint32_t i = 0; i < count; i++)
        {
            // if display is secondary mirror of another display, skip it
            if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)
                continue;

            CGRect displayRect = CGDisplayBounds(displays[i]);
            displayRect.origin.y = CGRectGetMaxY(primaryDisplayRect) - CGRectGetMaxY(displayRect);
            CGImageRef image = CGDisplayCreateImage(displays[i]);
            if (!image)
                continue;

            CGRect dest = CGRectMake(displayRect.origin.x - rect.origin.x,
                                     displayRect.origin.y - rect.origin.y,
                                     displayRect.size.width,
                                     displayRect.size.height);
            CGContextDrawImage(cgcontext, dest, image);
            CGImageRelease(image);
        }

        [[NSGraphicsContext currentContext] flushGraphics];
    }
    [NSGraphicsContext restoreGraphicsState];


    NSData* data = [imageRep representationUsingType:NSPNGFileType properties:@{ }];
    [data writeToFile:@"/tmp/screenshot.png" atomically:YES];

The main possible point of failure is in allocating a bitmap image context for a rectangle large enough to encompass all displays. Note that the total rect for all displays can be much larger than the rect for any one. For example, if two monitors are arranged so that they barely touch at a corner, the rectangle encompassing them would be nearly as big as four monitors in a 2x2 arrangement. For three monitors, it can be as big as 9 monitors in a 3x3 arrangement. Etc.


Here's an implementation that doesn't use Cocoa, just Core Graphics:

    CGDirectDisplayID displays[32];
    uint32_t count;
    if (CGGetActiveDisplayList(sizeof(displays)/sizeof(displays[0]), displays, &count) != kCGErrorSuccess)
    {
        NSLog(@"failed to get display list");
        exit(EXIT_FAILURE);
    }

    CGRect rect = CGRectNull;
    for (uint32_t i = 0; i < count; i++)
    {
        // if display is secondary mirror of another display, skip it
        if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)
            continue;

        rect = CGRectUnion(rect, CGDisplayBounds(displays[i]));
    }

    CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    if (!colorspace)
    {
        NSLog(@"failed to create colorspace");
        exit(EXIT_FAILURE);
    }

    CGContextRef cgcontext = CGBitmapContextCreate(NULL, CGRectGetWidth(rect), CGRectGetHeight(rect), 8, 0, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorspace);
    if (!cgcontext)
    {
        NSLog(@"failed to create bitmap context");
        exit(EXIT_FAILURE);
    }

    CGContextClearRect(cgcontext, CGRectMake(0, 0, CGRectGetWidth(rect), CGRectGetHeight(rect)));

    for (uint32_t i = 0; i < count; i++)
    {
        // if display is secondary mirror of another display, skip it
        if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)
            continue;

        CGRect displayRect = CGDisplayBounds(displays[i]);
        CGImageRef image = CGDisplayCreateImage(displays[i]);
        if (!image)
            continue;

        CGRect dest = CGRectMake(displayRect.origin.x - rect.origin.x,
                                 displayRect.origin.y - rect.origin.y,
                                 displayRect.size.width,
                                 displayRect.size.height);
        CGContextDrawImage(cgcontext, dest, image);
        CGImageRelease(image);
    }

    CGImageRef image = CGBitmapContextCreateImage(cgcontext);
    CGContextRelease(cgcontext);
    if (!image)
    {
        NSLog(@"failed to create image from bitmap context");
        exit(EXIT_FAILURE);
    }

    CFURLRef url = CFURLCreateWithFileSystemPath(NULL, CFSTR("/tmp/screenshot.png"), kCFURLPOSIXPathStyle, NO);
    if (!url)
    {
        NSLog(@"failed to create URL");
        exit(EXIT_FAILURE);
    }

    CGImageDestinationRef dest = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL);
    CFRelease(url);
    if (!dest)
    {
        NSLog(@"failed to create image destination");
        exit(EXIT_FAILURE);
    }

    CGImageDestinationAddImage(dest, image, NULL);
    CGImageRelease(image);
    if (!CGImageDestinationFinalize(dest))
    {
        NSLog(@"failed to finalize image destination");
        exit(EXIT_FAILURE);
    }
    CFRelease(dest);
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • 1
    thank you very much for your work! I will necessarily test this code on multiple monitors system!)))) And then write the result here! Thank you again))) – neo Jan 31 '15 at 15:20
  • 1
    What are function's analogues in CoreFoundation: 1. graphicsContextWithBitmapImageRep 2. saveGraphicsState 3. representationUsingType – neo Feb 03 '15 at 08:24
  • 1
    You can create a `CGContext` using `CGBitmapContextCreate()`. In that case, you would not use `NSBitmapImageRep` or `NSGraphicsContext`. After you draw the individual images into the context, you would ask it to create a `CGImage` with its contents using `CGBitmapContextCreateImage()`. Then, you would create a `CGImageDestination` similar to what you had originally done and add that image. As I commented earlier, the `CGImageDestination` should target a URL directly if you're just going to write it to file. The Cocoa stuff I used is mostly just wrapping those Core Graphics functions. – Ken Thomases Feb 03 '15 at 09:14
  • 1
    I did all that you told, but unfortunately, I get error in function CGBitmapContextCreate: //Create bmp context for image: CGContextRef context = CGBitmapContextCreate(NULL, CGRectGetWidth(rect), CGRectGetHeight(rect), 8, CGRectGetHeight(rect) * 24, colorSpace, kCGBitmapAlphaInfoMask); – neo Feb 03 '15 at 11:04
  • 1
    You are computing the `bytesPerRow` incorrectly. Best to just pass 0 and let it pick an optimal value. Also, you should not be passing `kCGBitmapAlphaInfoMask` for the `bitmapInfo`. That mask just indicates which bits within a bitmap info value have to do with the alpha. You want to pass a specific alpha info value. I have updated my answer with an implementation that doesn't use Cocoa, just Core Graphics. – Ken Thomases Feb 03 '15 at 13:44
  • Wow oh wow thank you for both cocoa and CG method, what is the lowest OSX version that supports this? Thank you very much I'm going to start writing it up in jsctypes and share with you how it goes. Do you have firefox? I would really really love to show you this simple screenshot addon you help me a lot and I want to show you all your help is helping me make super firefox addons! :D – Noitidart May 09 '15 at 22:37
  • Also quick q @KenThomases instead of drawing the image i need to get a RGBA byte (uint8_t[]) array out of it. I'm in the middle of porting the above cocoa version but just wanted to ask you if you knew off the top of your head but if not nooo problem you did too much already Ill search soon after i finish the port :D – Noitidart May 09 '15 at 23:38
  • 1
    Well, you can use `[imageRep bitmapData]` to get the bytes. If you do that, you should either pass an explicit `bytesPerRow` (rather than 0 as in my code) or your should query `imageRep.bytesPerRow` to learn what was used, depending on if the code that uses the byte array can take an arbitrary bytes-per-row layout. Also note that the bitmap data color components have been premultiplied with the alpha. If you need unpremultiplied data, you'll have to convert it. – Ken Thomases May 10 '15 at 01:01
  • Also @Ken there is a bit of a typo i think ` [NSGraphicsContext saveGraphicsState]; {` is there supposed to be an `if` there? – Noitidart May 10 '15 at 01:55
  • 1
    No. I use a compound statement just to set apart the code which is between the saving and restoring of the graphics context state. – Ken Thomases May 10 '15 at 01:57
  • Hi @ken so i fixed it! The mistake was sooo silly: http://stackoverflow.com/a/30173487/1828637 Please see screenshot I took with it here: http://i.imgur.com/n7lSxje.png I have a question and I think it fits here. Please see the uploaded screenshot, my mouse is visible, anyway to make it not show the mouse? Shall we also go through and clean up this comments section? Ill start deleting my stuff :) I'll leave the question about getting byte array can you please leave your reply to it, as i think thats relavent right? – Noitidart May 11 '15 at 17:08
  • 1
    You can't exclude the cursor from the screen image, as such. However, you can hide it using `CGDisplayHideCursor()` before capturing the image and then show it again using `CGDisplayShowCursor()`. – Ken Thomases May 11 '15 at 18:19
  • Thanks @KenThomases it worked!! https://github.com/Noitidart/NativeShot/blob/master/modules/workers/MainWorker.js#L574-L810 I used `4 * CGRectGetWidth(rect)` and then I used `[imageRep bitmapData]` which I could then `cast` to `unsigned_char` of length `4 * CGRectGetWidth(rect) * CGRectGetHeight(rect)`, was this the right way to do it? :) – Noitidart Jul 10 '15 at 15:15
  • Hey @Ken using this code the coloring is a bit darker then it is normally, is this a known thing? Here is a screenshot of the difference in color: http://i.imgur.com/rm7ZcvQ.png see the dark blue on top and the light blue next to dock. they are supposed to be same color, the actual is the light blue, and the dark blue is a canvas with the image from this method – Noitidart Sep 19 '15 at 16:24
  • 1
    @Noitidart, post a new question. – Ken Thomases Sep 19 '15 at 16:34
  • Hey Ken, when I `CGRectUnion` these monitors, its not respecting my physical setup. Like my physical setup as the tops of the monitor aligned as seen in this image: http://i.imgur.com/2kdstNU.png but when i take screenshot with this method, it aligns the bottoms, do you have any ideas on how to make the positions of the window in the `CGRectUnion`ed rect to be at proper positions of screen x and y? edit: ah its like opposite, when i aligned to bottoms physically, then the left monitor (smaller) came out top aligned, do you know whats up? – Noitidart Sep 21 '15 at 16:01
  • Like see, I physically aligned the bottoms as seen here: http://i.imgur.com/1dZshEx.png but the screenshot came out with the tops lined up: http://i.imgur.com/ZUjodtI.png – Noitidart Sep 21 '15 at 16:17
  • 1
    My first approach using `NSBitmapImageRep` did get the origins wrong. The context has a transform applied to flip its coordinate system because Cocoa's is different than Core Graphics'. I forgot to compensate for that. I have edited my answer to fix it. – Ken Thomases Sep 21 '15 at 17:54
  • Hey Ken Im looking at your update, Im curiours how come you didnt apply the same method of correction on the x? – Noitidart Sep 21 '15 at 18:03
  • 1
    Oops! Still had it wrong. Properly fixed and tested now. It's only done for the Y coordinate because that's the only difference between Cocoa's coordinate system and Core Graphics' coordinate system. – Ken Thomases Sep 21 '15 at 18:56
  • Ahhh very interesting about the y coord diff betwee ncocoa and cg, I'll test and report back as soon as I get access, thanks so much Ken!! :) – Noitidart Sep 22 '15 at 00:04
  • Hey @KenThomases isnt this impossible `if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay)` when i == 0? as that is always the primary for sure? Im just doing some optimizations while i wait around for this guy to come online :) – Noitidart Sep 22 '15 at 05:32
  • 1
    I'm not sure. With hardware mirroring, the secondary displays in mirror sets aren't listed by `CGGetActiveDisplayList()`. With software mirroring, they are listed and the biggest or deepest-color display is listed first, but it's not clear that that's always the "primary" of the mirror set. If the first is *not* the primary, then my code fails to figure out the `primaryDisplayRect` (because it skips the 0th display before setting that), which is a bug. Instead of checking `i == 0`, it could check if `CGRectIsEmpty(primaryDisplayRect)` to decide if it still needs to set that. – Ken Thomases Sep 22 '15 at 05:57
  • Thanks I like that idea of doing `CGRectIsEmpty` so the first non-mirror is guranteed to be the primary true? Aside: I just tested your updated code and it works beutifuly! Thank you so much!! – Noitidart Sep 22 '15 at 09:55