From http://www.jwz.org/xscreensaver/xscreensaver-5.40.tar.gz
[xscreensaver] / OSX / grabclient-osx.m
1 /* xscreensaver, Copyright (c) 1992-2018 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* This is the OSX implementation of desktop-grabbing and image-loading.
13    This code is invoked by "utils/grabclient.c", which is linked directly
14    in to each screen saver bundle.
15
16    X11-based builds of the savers do not use this code (even on MacOS).
17    This is used only by the Cocoa build of the savers.
18  */
19
20 #import <stdlib.h>
21 #import <stdint.h>
22 #ifndef USE_IPHONE
23 # import <Cocoa/Cocoa.h>
24 #else
25 # import "SaverRunner.h"
26 #endif
27 #import "jwxyz-cocoa.h"
28 #import "grabscreen.h"
29 #import "colorbars.h"
30 #import "resources.h"
31 #import "usleep.h"
32
33
34 #ifdef USE_IPHONE
35 # define NSImage UIImage
36 #endif
37
38
39 #ifndef  MAC_OS_X_VERSION_10_6
40 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
41 #endif
42
43 #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
44
45      /* 10.4 code.
46
47         This version of the code works on 10.4, but is flaky.  There is
48         a better way to do it on 10.5 and newer, but taking this path,
49         then we are being compiled against the 10.4 SDK instead of the
50         10.5 SDK, and the newer API is not available to us.
51       */
52
53 static void
54 copy_framebuffer_to_ximage (CGDirectDisplayID cgdpy, XImage *xim,
55                             int window_x, int window_y)
56 {
57   unsigned char *data = (unsigned char *) 
58     CGDisplayAddressForPosition (cgdpy, window_x, window_y);
59   int bpp = CGDisplayBitsPerPixel (cgdpy);
60   int spp = CGDisplaySamplesPerPixel (cgdpy);
61   int bps = CGDisplayBitsPerSample (cgdpy);
62   int bpr = CGDisplayBytesPerRow (cgdpy);
63
64   int y;
65   int ximw = xim->width;
66   int ximh = xim->height;
67
68   uint32_t *odata = (uint32_t *) xim->data;
69
70   switch (bpp) {
71   case 32:
72     if (spp != 3) abort();
73     if (bps != 8) abort();
74     int xwpl = xim->bytes_per_line/4;
75     for (y = 0; y < ximh; y++) {
76       // We can do this because the frame buffer and XImage are both ARGB 32.
77       // Both PPC and Intel use ARGB, viewed in word order (not byte-order).
78       memcpy (odata, data, ximw * 4);
79       odata += xwpl;
80       data += bpr;
81     }
82     break;
83
84   case 16:
85     if (spp != 3) abort();
86     if (bps != 5) abort();
87     for (y = 0; y < ximh; y++) {
88       uint16_t *ip = (uint16_t *) data;
89       int x;
90       for (x = 0; x < ximw; x++) {
91         uint16_t p = *ip++;
92         // This should be ok on both PPC and Intel (ARGB, word order)
93         unsigned char r = (p >> 10) & 0x1F;
94         unsigned char g = (p >>  5) & 0x1F;
95         unsigned char b = (p      ) & 0x1F;
96         r = (r << 3) | (r >> 2);
97         g = (g << 3) | (g >> 2);
98         b = (b << 3) | (b >> 2);
99         uint32_t pixel = 0xFF000000 | (r << 16) | (g << 8) | b;
100         // XPutPixel (xim, x, y, pixel);
101         *odata++ = pixel;
102       }
103       data += bpr;
104     }
105     break;
106
107   case 8:
108     {
109       /* Get the current palette of the display. */
110       CGDirectPaletteRef pal = CGPaletteCreateWithDisplay (cgdpy);
111
112       /* Map it to 32bpp pixels */
113       uint32_t map[256];
114       for (y = 0; y < 256; y++) {
115         CGDeviceColor c = CGPaletteGetColorAtIndex (pal, y);
116         unsigned char r = c.red   * 255.0;
117         unsigned char g = c.green * 255.0;
118         unsigned char b = c.blue  * 255.0;
119         uint32_t pixel = 0xFF000000 | (r << 16) | (g << 8) | b;
120         map[y] = pixel;
121       }
122
123       for (y = 0; y < ximh; y++) {
124         unsigned char *ip = data;
125         int x;
126         for (x = 0; x < ximw; x++) {
127           *odata++ = map[*ip++];
128         }
129         data += bpr;
130       }
131       CGPaletteRelease (pal);
132     }
133     break;
134
135   default:
136     abort();
137     break;
138   }
139 }
140
141
142 /* Loads an image into the Drawable, returning once the image is loaded.
143  */
144 Bool
145 osx_grab_desktop_image (Screen *screen, Window xwindow, Drawable drawable,
146                         XRectangle *geom_ret)
147 {
148   Display *dpy = DisplayOfScreen (screen);
149   NSView *nsview = jwxyz_window_view (xwindow);
150   NSWindow *nswindow = [nsview window];
151   XWindowAttributes xgwa;
152   int window_x, window_y;
153   Window unused;
154
155   // Figure out where this window is on the screen.
156   //
157   XGetWindowAttributes (dpy, xwindow, &xgwa);
158   XTranslateCoordinates (dpy, xwindow, RootWindowOfScreen (screen), 0, 0, 
159                          &window_x, &window_y, &unused);
160
161   // Use the size of the Drawable, not the Window.
162   {
163     Window r;
164     int x, y;
165     unsigned int w, h, bbw, d;
166     XGetGeometry (dpy, drawable, &r, &x, &y, &w, &h, &bbw, &d);
167     xgwa.width = w;
168     xgwa.height = h;
169   }
170
171   // Create a tmp ximage to hold the screen data.
172   //
173   XImage *xim = XCreateImage (dpy, xgwa.visual, 32, ZPixmap, 0, 0,
174                               xgwa.width, xgwa.height, 8, 0);
175   xim->data = (char *) malloc (xim->height * xim->bytes_per_line);
176
177
178   // Find the address in the frame buffer of the top left of this window.
179   //
180   CGDirectDisplayID cgdpy = 0;
181   {
182     CGPoint p;
183     // #### this isn't quite right for screen 2: it's offset slightly.
184     p.x = window_x;
185     p.y = window_y;
186     CGDisplayCount n;
187     CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
188     if (!cgdpy) abort();
189   }
190
191   // Paint a transparent "hole" in this window.
192   //
193   BOOL oopaque = [nswindow isOpaque];
194   [nswindow setOpaque:NO];
195
196   [[NSColor clearColor] set];
197   NSRectFill ([nsview frame]);
198   [[nswindow graphicsContext] flushGraphics];
199
200
201   // Without this, we get a dozen black scanlines at the top.
202   // #### But with this, the screen saver loops, because calling this
203   //      seems to implicitly mark the display as non-idle!
204   // CGDisplayCaptureWithOptions (cgdpy, kCGCaptureNoFill);
205
206   // #### So let's try waiting for the vertical blank instead...
207   //      Nope, that doesn't work.
208   //
209   // CGDisplayWaitForBeamPositionOutsideLines (cgdpy, 0,
210   //   window_y + [nswindow frame].size.height);
211
212   // #### Ok, try a busy-wait?
213   //      Nope.
214   //
215
216   // #### Ok, just fuckin' sleep!
217   //
218   usleep (100000);
219
220
221   // Pull the bits out of the frame buffer.
222   //
223   copy_framebuffer_to_ximage (cgdpy, xim, window_x, window_y);
224
225   // CGDisplayRelease (cgdpy);
226
227   // Make the window visible again.
228   //
229   [nswindow setOpaque:oopaque];
230
231   // Splat the XImage onto the target drawable (probably the window)
232   // and free the bits.
233   //
234   XGCValues gcv;
235   GC gc = XCreateGC (dpy, drawable, 0, &gcv);
236   XPutImage (dpy, drawable, gc, xim, 0, 0, 0, 0, xim->width, xim->height);
237   XFreeGC (dpy, gc);
238
239   if (geom_ret) {
240     geom_ret->x = 0;
241     geom_ret->y = 0;
242     geom_ret->width  = xim->width;
243     geom_ret->height = xim->height;
244   }
245
246   XDestroyImage (xim);
247   return True;
248 }
249
250
251 #elif defined(USE_IPHONE)
252
253         /* What a hack!
254
255            On iOS, our application delegate, SaverRunner, grabs an image
256            of itself as a UIImage before mapping the XScreenSaverView.
257            In this code, we ask SaverRunner for that UIImage, then copy
258            it to the root window.
259          */
260
261 Bool
262 osx_grab_desktop_image (Screen *screen, Window xwindow, Drawable drawable,
263                         XRectangle *geom_ret)
264 {
265   SaverRunner *s = 
266     (SaverRunner *) [[UIApplication sharedApplication] delegate];
267   if (! s)
268     return False;
269   if (! [s isKindOfClass:[SaverRunner class]])
270     return False;
271   UIImage *img = [s screenshot];
272   if (! img)
273     return False;
274   jwxyz_draw_NSImage_or_CGImage (DisplayOfScreen (screen), drawable,
275                                  True, img, geom_ret, 0);
276   return True;
277 }
278
279
280 #else /* MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
281
282          10.5+ code.
283
284          This version of the code is simpler and more reliable, but
285          uses an API that only exist on 10.5 and newer, so we can only
286          use it if when being compiled against the 10.5 SDK or later.
287        */
288
289 extern float jwxyz_scale (Window);  /* jwxyzI.h */
290
291 /* Loads an image into the Drawable, returning once the image is loaded.
292  */
293 Bool
294 osx_grab_desktop_image (Screen *screen, Window xwindow, Drawable drawable,
295                         XRectangle *geom_ret)
296 {
297   Display *dpy = DisplayOfScreen (screen);
298   NSView *nsview = jwxyz_window_view (xwindow);
299   XWindowAttributes xgwa;
300   int window_x, window_y;
301   Window unused;
302
303   // Figure out where this window is on the screen.
304   //
305   XGetWindowAttributes (dpy, xwindow, &xgwa);
306   XTranslateCoordinates (dpy, xwindow, RootWindowOfScreen (screen), 0, 0, 
307                          &window_x, &window_y, &unused);
308
309   // Grab only the rectangle of the screen underlying this window.
310   //
311   CGRect cgrect;
312   double s = jwxyz_scale (xwindow);
313   cgrect.origin.x    = window_x;
314   cgrect.origin.y    = window_y;
315   cgrect.size.width  = xgwa.width  / s;
316   cgrect.size.height = xgwa.height / s;
317
318   /* If a password is required to unlock the screen, a large black
319      window will be on top of all of the desktop windows by the time
320      we reach here, making the screen-grab rather uninteresting.  If
321      we move ourselves temporarily below the login-window windows
322      before capturing the image, we capture the real desktop as
323      intended.
324
325      Oct 2016: Surprise, this trick no longer works on MacOS 10.12.  Sigh.
326    */
327
328   CGWindowID windowNumber = (CGWindowID) nsview.window.windowNumber;
329
330   {
331     CFArrayRef L = CGWindowListCopyWindowInfo (kCGWindowListOptionOnScreenOnly,
332                                                kCGNullWindowID);
333
334     CFIndex n = CFArrayGetCount(L);
335     for (int i = 0; i < n; i++) {
336       NSDictionary *dict = (NSDictionary *) CFArrayGetValueAtIndex(L, i);
337
338       // loginwindow creates multiple toplevel windows. Grab the lowest one.
339       if(![([dict objectForKey:(NSString *)kCGWindowOwnerName])
340            compare:@"loginwindow"]) {
341         windowNumber = ((NSNumber *)[dict objectForKey:
342                                      (NSString *)kCGWindowNumber]).intValue;
343       }
344     }
345     CFRelease (L);
346   }
347
348   // Grab a screen shot of those windows below this one
349   // (hey, X11 can't do that!)
350   //
351   CGImageRef img = 
352     CGWindowListCreateImage (cgrect,
353                              kCGWindowListOptionOnScreenBelowWindow,
354                              windowNumber,
355                              kCGWindowImageDefault);
356
357   if (! img) return False;
358
359   // Render the grabbed CGImage into the Drawable.
360   jwxyz_draw_NSImage_or_CGImage (DisplayOfScreen (screen), drawable, 
361                                  False, img, geom_ret, 0);
362   CGImageRelease (img);
363   return True;
364 }
365
366 #endif /* 10.5+ code */
367
368
369 # ifndef USE_IPHONE
370
371 /* Returns the EXIF rotation property of the image, if any.
372  */
373 static int
374 exif_rotation (const char *filename)
375 {
376   /* As of 10.6, NSImage rotates according to EXIF by default:
377      http://developer.apple.com/mac/library/releasenotes/cocoa/appkit.html
378      So this function should return -1 when *running* on 10.6 systems.
379      But when running against older systems, we need to examine the image
380      to figure out its rotation.
381    */
382
383 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
384
385   /* When we have compiled against the 10.6 SDK, we know that we are 
386      running on a 10.6 or later system.
387    */
388   return -1;
389
390 # else /* Compiled against 10.5 SDK or earlier */
391
392   /* If this selector exists, then we are running against a 10.6 runtime
393      that does automatic EXIF rotation (despite the fact that we were
394      compiled against the 10.5 or earlier SDK).  So in that case, this
395      function should no-op.
396    */
397   if ([NSImage instancesRespondToSelector:
398                  @selector(initWithDataIgnoringOrientation:)])
399     return -1;
400
401   /* Otherwise, go ahead and figure out what the rotational characteristics
402      of this image are. */
403
404
405
406   /* This is a ridiculous amount of rigamarole to go through, but for some
407      reason the "Orientation" tag does not exist in the "NSImageEXIFData"
408      dictionary inside the NSBitmapImageRep of the NSImage.  Several other
409      EXIF tags are there (e.g., shutter speed) but not orientation.  WTF?
410    */
411   CFStringRef s = CFStringCreateWithCString (NULL, filename, 
412                                              kCFStringEncodingUTF8);
413   CFURLRef url = CFURLCreateWithFileSystemPath (NULL, s, 
414                                                 kCFURLPOSIXPathStyle, 0);
415   CGImageSourceRef cgimg = CGImageSourceCreateWithURL (url, NULL);
416   if (! cgimg) return -1;
417
418   NSDictionary *props = (NSDictionary *)
419     CGImageSourceCopyPropertiesAtIndex (cgimg, 0, NULL);
420   int rot = [[props objectForKey:@"Orientation"] intValue];
421   CFRelease (cgimg);
422   CFRelease (url);
423   CFRelease (s);
424   return rot;
425
426 # endif /* 10.5 */
427 }
428
429 # endif /* USE_IPHONE */
430
431
432
433 /* Loads an image file and splats it onto the drawable.
434    The image is drawn as large as possible while preserving its aspect ratio.
435    If geom_ret is provided, the actual rectangle the rendered image takes
436    up will be returned there.
437  */
438 Bool
439 osx_load_image_file (Screen *screen, Window xwindow, Drawable drawable,
440                      const char *filename, XRectangle *geom_ret)
441 {
442 # ifndef USE_IPHONE
443
444   if (!filename || !*filename) return False;
445
446   NSImage *img = [[NSImage alloc] initWithContentsOfFile:
447                                     [NSString stringWithCString:filename
448                                               encoding:NSUTF8StringEncoding]];
449   if (!img)
450     return False;
451
452   jwxyz_draw_NSImage_or_CGImage (DisplayOfScreen (screen), drawable, 
453                                  True, img, geom_ret,
454                                  exif_rotation (filename));
455   [img release];
456   return True;
457
458 # else  /* USE_IPHONE */
459
460   /* This is handled differently: see grabclient.c and grabclient-ios.m. */
461   return False;
462
463 # endif /* USE_IPHONE */
464 }