Skip to content

Instantly share code, notes, and snippets.

@leptos-null
Last active April 29, 2021 12:26
Show Gist options
  • Save leptos-null/c88e8c3c0f9f6860c7500826031c8551 to your computer and use it in GitHub Desktop.
Save leptos-null/c88e8c3c0f9f6860c7500826031c8551 to your computer and use it in GitHub Desktop.
Mojave Dynamic Desktop- How it works

Mojave Dynamic Desktop- How it works

Prompted by a tweet by NSHipster, and a subsequent thread, I wanted to find out how Mojave dynamic wallpapers worked. NSHipster and ole reverse engineered the file format. In the Twitter thread, NSHipster mentions an edge case: What happens above 66ºN (latitude)?

The first thing to do was find out what process handles the wallpaper on macOS. I primarily do iOS research, and honestly had no idea. I opened Console, searched for "solar", and then changed my static wallpaper to a dynamic one. Voila!

Message: index: 7 next: 14815.999366

Process: Dock

Image: DynamicDesktop

Category: solar

Domain: com.apple.dynamicdesktop

otool -L /System/Library/PrivateFrameworks/DynamicDesktop.framework/DynamicDesktop confirms my suspicions that the GeoServices framework is being used for solar calculations (used on iOS by Home app and backboardd for sunrise and sunset, and on watchOS for the Solar watch face and the lunar complication).

I wrote a test program to find out what GeoServices says sunrise and sunset is at problematically high latitudes. The magic latitude was 86.2ºN. Once 86.21º was hit, the NSDates for the rise and set properties of GEOCelestialEphemeris were nil.

NSHipster followed up with a reasonable question: "[D]oes that mean that our friends at the ends of the Earth are indeed left in the dark with Dynamic Desktop?" To answer this, we need to have a look at the code. How does Apple handle these edge cases?

Bonus: How much of the Earth falls into this area (North of 86.2ºN)?

Using the Spherical Cap formula, we get 4.1581e+6 square miles. Google has the radius of Earth at 3,959 mi. I calculated the height by using radius - (radius/90 * 86.2) (seems sound, please comment below, or tweet at me if you think otherwise). This comes out to 2.1% of Earth's total surface area. We need to multiply this by two though, to get the South Pole: 4.2% of Earth's total surface area is not supported by GEOCelestialEphemeris.

Opening up the private DynamicDesktop framework in Hopper, the first thing I noticed is that it's written in Swift. This isn't relevant to this write-up, however I wanted to note it, because I rarely see Swift in frameworks shipped with iOS.

I'm writing as I'm going through this process. I just realized a mistake I made: The write-ups by Ole and NSHipster mention the altitude and azimuth. The API I tested is for getting sunrise and sunset times, and I was assuming some simple math would be applied to get the altitude. Obtaining the azimuth in this manner is not feasible. The code in DynamicDesktop confirms that.

The test program I mentioned above used the GEOCelestialEphemeris class. DynamicDesktop uses GEOHorizontalCelestialBodyData, which has two properties: altitude and azimuth. Gently hits head. An updated test program will be attached below.

Conclusion

DynamicDesktop is the framework responsible for dynamic wallpapers. It uses the GEOHorizontalCelestialBodyData class from GeoServices, a framework used on macOS, iOS, and watchOS to power various features. The altitude and azimuth of the sun is able to be calculated for any latitude on Earth.

//
// solar.m
// geoservices-example
//
// Created by Leptos on 10/6/18.
// Copyright © 2018 Leptos. All rights reserved.
//
#import "GeoServices/GEOHorizontalCelestialBodyData.h"
// clone https://github.com/leptos-null/geoservices-example and put this file inside, compile using:
// clang solar.m -fobjc-arc -iframework /System/Library/PrivateFrameworks -framework Foundation -framework GeoServices -o solar_limits
int main() {
CLLocationCoordinate2D coordinates;
coordinates.longitude = 0;
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeStyle = NSDateFormatterShortStyle;
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
NSDate *date = [NSDate date];
for (CLLocationDegrees latitude = 50; latitude <= 90; latitude += 5) {
coordinates.latitude = latitude;
GEOHorizontalCelestialBodyData *celestialBodyData = [[GEOHorizontalCelestialBodyData alloc] initWithLocation:coordinates date:date body:GEOCelestialBodySun];
printf("Latitude: %+.2f\n"
"Date: %s\n"
"altitude: %.4f\n"
"azimuth: %.4f\n"
"\n", latitude, [[dateFormatter stringFromDate:date] UTF8String], celestialBodyData.altitude, celestialBodyData.azimuth);
}
}
//
// Created by Leptos on 10/6/18.
// Copyright © 2018 Leptos. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
static const CFStringRef AppleDesktopSolarKey = CFSTR("apple_desktop:solar");
static const CFStringRef AppleDesktopDayKey = CFSTR("apple_desktop:h24"); /* not used (by Apple) yet */
static const CFStringRef AppleDesktopAprKey = CFSTR("apple_desktop:apr");
static NSDictionary *decodeForKey(CGImageMetadataRef metadata, CFStringRef key) {
NSDictionary *ret = nil;
CGImageMetadataTagRef tag = CGImageMetadataCopyTagWithPath(metadata, NULL, key);
if (tag) {
CFStringRef value = CGImageMetadataTagCopyValue(tag);
assert(value);
NSData *decoded = [[NSData alloc] initWithBase64EncodedString:(__bridge NSString *)value options:0];
ret = [NSPropertyListSerialization propertyListWithData:decoded options:0 format:NULL error:NULL];
CFRelease(value);
CFRelease(tag);
}
return ret;
}
int main(int argc, const char *argv[]) {
const char *argOne = argv[1];
if (argOne == NULL) {
printf("Usage: %s <path>\n", argv[0]);
return 1;
}
NSURL *url = [NSURL fileURLWithPath:@(argOne)];
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
if (source == NULL) {
printf("Unable to create image from '%s'\n", argOne);
return 1;
}
CGImageMetadataRef metadata = CGImageSourceCopyMetadataAtIndex(source, 0, NULL);
NSDictionary *decodedSolar = decodeForKey(metadata, AppleDesktopSolarKey);
NSDictionary *decodedDay = decodeForKey(metadata, AppleDesktopDayKey);
NSDictionary *decodedApr = decodeForKey(metadata, AppleDesktopAprKey);
CFRelease(metadata);
CFRelease(source);
/*
* thanks to https://twitter.com/Folletto/status/1142600707226386432 for "ti"
* thanks to https://gist.github.com/leptos-null/c88e8c3c0f9f6860c7500826031c8551#gistcomment-3508851 for "ap"
*
* solar: {
* "si": (
* {
* "a": CLLocationDegrees (altitude)
* "z": CLLocationDegrees (azimuth)
* "i": uint (index)
* }
* ) (solar information)
* "ap": {
* "l": uint (light, the index to use for light mode)
* "d": uint (dark, the index to use for dark mode)
* } (appearence)
* }
* h24: {
* "ti": {
* "i": uint (index)
* "t": float (time, percentage into 24 hour day)
* } (time information)
* }
* apr: {
* "l": uint (light, the index to use for light mode)
* "d": uint (dark, the index to use for dark mode)
* }
*/
NSString *formatted = [NSString stringWithFormat:@"solar: %@\n"
"h24: %@\n"
"apr: %@\n",
decodedSolar, decodedDay, decodedApr];
printf("%s", formatted.UTF8String);
return 0;
}
@phanirithvij
Copy link

I see these kinds of files

  "ap": {
        "d": 3,
        "l": 2
    }

So they might not be bool but the indexes of the light and dark images I think.

@leptos-null
Copy link
Author

@phanirithvij thanks! I agree. I've updates the gist.

@phanirithvij
Copy link

Can you give a link to an example file for apr type

@leptos-null
Copy link
Author

@phanirithvij the paths below are to files that I found on macOS 11.3 with the apr type:

/System/Library/Desktop Pictures/Big Sur Graphic.heic
/System/Library/Desktop Pictures/Dome.heic
/System/Library/Desktop Pictures/Iridescence.heic
/System/Library/Desktop Pictures/Peak.heic
/System/Library/Desktop Pictures/Tree.heic
/System/Library/Desktop Pictures/Valley.heic
/System/Library/Desktop Pictures/hello Blue.heic
/System/Library/Desktop Pictures/hello Green.heic
/System/Library/Desktop Pictures/hello Grey.heic
/System/Library/Desktop Pictures/hello Orange.heic
/System/Library/Desktop Pictures/hello Purple.heic
/System/Library/Desktop Pictures/hello Red.heic
/System/Library/Desktop Pictures/hello Yellow.heic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment