Translator: saitjr. Proofreading: Winter melon, Yousanflics; Finalized: Forelax

Dark Mode is one of the most popular features of macOS — especially for developers like you and me. Not only do we like the dark theme of the text editor, but we also like the consistency of the tone throughout the system.

For the past few years, this feature has been matched by the Night Shift, which reduces eye strain as day and Night Shift.

Dynamic Desktop is one of the new Mojave features. Go to System Preferences > Desktop & Screensaver and select Dynamic to get a location-based wallpaper that changes dynamically throughout the day.

The effect is subtle and pleasant. The tabletop seems to have been given life and can change over time; Conform to the laws of nature. (If not surprisingly, there will be delightful special effects combined with the dark mode switch.)

How exactly does this work? That’s what NSHipster discussed this week.

The answers will delve into image formats and involve some reverse engineering and spherical trigonometry.




The first step to understanding Dynamic Desktop is to find these Dynamic images.

In macOS Mojave, open access and choose Go > Go folder… ⇧⌘G), type “/Library/Desktop Pictures/”.

In this directory, you will find a file named “mojaave.heic”. Double-click to open by preview.

In the preview, thumbnails ranging from 1 to 16 are displayed on the left side, each showing a different state of the desert.

More detailed information can be seen if you choose Tools > Show Inspector (⌘I), as shown below:

Unfortunately, that’s all the preview shows (as of press time). Even if we click on the “More Information checker” next to it, all we get is the following table. The rest is unknown:

Color Model RGB
Depth: 8
Pixel Height 2880
Pixel Width 5120
Profile Name Display P3

The suffix. Heic indicates that the Image container is encoded by high-efficiency Image File Format (HFIF), which is based on high-efficiency Video Compression (HEVC). High efficiency video compression, h.265). For more information, see WWDC 2017 Session 503 “Introducing HEIF and HEVC”

To get more data, we need to really dig into the underlying API.

Find out with CoreGraphics

The first step is to create the Xcode Playground. For simplicity, we hardcoded the “mojave. heic” file path into our code.

import Foundation
import CoreGraphics

MacOS 10.14 Mojave is required
let url = URL(fileURLWithPath: "/Library/Desktop Pictures/Mojave.heic")
Copy the code

Then, create the CGImageSource, copy the metadata and iterate through all the tags:

let source = CGImageSourceCreateWithURL(url as CFURL.nil)!
let metadata = CGImageSourceCopyMetadataAtIndex(source, 0.nil)!
let tags = CGImageMetadataCopyTags(metadata) as! [CGImageMetadataTag]
for tag in tags {
    guard let name = CGImageMetadataTagCopyName(tag),
        let value = CGImageMetadataTagCopyValue(tag)
    else {
        continue
    }

    print(name, value)
}
Copy the code

Running this code yields two values: hasXMP, which is “True”, and solar, which is a string of unintelligible data:

YnBsaXN0MDDRAQJSc2mvEBADDBAUGBwgJCgsMDQ4PEFF1AQFBgcICQoLUWlRelFh
UW8QACNAcO7vOubr3yO/1e+pmkOtXBAB1AQFBgcNDg8LEAEjQFRxqCKOFiAjwCR6
waUkDgHUBAUGBxESEwsQAiNAVZV4BI4c+CPAEP2uFrMcrdQEBQYHFRYXCxADI0BW
tALKmrjwIz/2ObLnx6l21AQFBgcZGhsLEAQjQFfTrJlEjnwjQByrLle1Q0rUBAUG
Bx0eHwsQBSNAWPrrmI0ISCNAKiwhpSRpc9QEBQYHISIjCxAGI0BgJff9KDpyI0BE
NTOsilht1AQFBgclJicLEAcjQGbHdYIVQKojQEq3fAg86lXUBAUGBykqKwsQCCNA
bTGmpC2YRiNAQ2WFOZGjntQEBQYHLS4vCxAJI0BwXfII2B+SI0AmLcjfuC7g1AQF
BgcxMjMLEAojQHCnF6YrsxcjQBS9AVBLTq3UBAUGBzU2NwsQCyNAcTcSnimmjCPA
GP5E0ASXJtQEBQYHOTo7CxAMI0BxgSADjxK2I8AoalieOTyE1AQFBgc9Pj9AEA0j
QHNWsnnMcWIjwEO+oq1pXr8QANQEBQYHQkNEQBAOI0ABZpkFpAcAI8BKYGg/VvMf
1AQFBgdGR0hAEA8jQErBKblRzPgjwEMGElBIUO0ACAALAA4AIQAqACwALgAwADIA
NAA9AEYASABRAFMAXABlAG4AcAB5AIIAiwCNAJYAnwCoAKoAswC8AMUAxwDQANkA
4gDkAO0A9gD/AQEBCgETARwBHgEnATABOQE7AUQBTQFWAVgBYQFqAXMBdQF+AYcB
kAGSAZsBpAGtAa8BuAHBAcMBzAHOAdcB4AHpAesB9AAAAAAAAAIBAAAAAAAAAEkA
AAAAAAAAAAAAAAAAAAH9
Copy the code

The light of the sun

When most people read that, they would silently close their MacBook Pro and shout goodbye. But someone must have noticed that this string of text looks a lot like a Base64 masterpiece.

Let’s test this hypothesis:

if name == "solar" {
    let data = Data(base64Encoded: value)!
    print(String(data: data, encoding: .ascii))
}
Copy the code


Bplist00O \ u {01} \ u {02} \ u {3}...


What’s this? Bplist followed by a bunch of gibberish, right?

Oh, my God, this is a file signature for the binary property list.

Using PropertyListSerialization to see…

if name == "solar" {
    let data = Data(base64Encoded: value)!
    let propertyList = try PropertyListSerialization
                            .propertyList(from: data,
                                          options: [],
                                          format: nil)
    print(propertyList)
}
Copy the code
(
    ap = {
        d = 15;
        l = 0;
    };
    si = (
        {
            a = "0.3427528387535028";
            i = 0;
            z = "270.9334057827345"; },... { a ="38.04743388682423";
            i = 15;
            z = "53.50908581251309"; }))Copy the code

Much clearer!

First there are two primary bonds:

The ap key corresponds to a dictionary containing the d and L keys, both of which are integers.

Si keys correspond to an array of dictionaries, ranging from integers to floating-point values. In nested dictionaries, I is easiest to understand: it increments from 0 to 15, which indicates the subscript of the sequence of pictures. Without more information, it’s hard to guess what a and Z mean, but they actually represent the height (a) and azimuth (z) of the sun in the corresponding image.

Calculate the position of the sun

As I write, people in the northern hemisphere are entering autumn with shorter days and cooler temperatures, while those in the southern hemisphere are experiencing longer days and warmer temperatures. The seasons tell us that the amount of daylight depends on where you are on the planet and how it orbits the sun.

The good news is that astronomers can tell you — and fairly accurately — where the sun is in the sky, or at what time. Unfortunately, the calculations are complicated.

But to be honest, we don’t have to dig too deep into it, you can find the code on the web. With trial and error, they work for me (welcome PR!). :

import Foundation
import CoreLocation

// Apple Park in Cupertino, California
let location = CLLocation(latitude: 37.3327, longitude: -122.0053)
let time = Date(a)let position = solarPosition(for: location, at: time)
let formattedDate = DateFormatter.localizedString(from: time,
                                                    dateStyle: .medium,
                                                    timeStyle: .short)
print("Solar Position on \(formattedDate)")
print("\(position.azimuth)° Az / \(position.elevation)° El. "")
Copy the code

Solar Position on Oct 1, 2018 at 12:00 180.73470025840783° Az / 49.27482549913847° El

At noon on October 1, 2018, the sun shone from the south over Apple Park, about halfway across the horizon and directly overhead.

If you plot the position of the sun for a day, you get a sinusoidal curve, reminiscent of the Apple Watch’s “solar dial.”

Expand your understanding of XMP

Well, astronomy is done. What follows is a tedious process: XML metadata in front of you.

Remember the metadata key hasXMP? Yes, that’s it.

The Extensible Metadata Platform (XMP) is a standard format that uses Metadata to mark files. What does XMP look like? Cheer up, please:

let xmpData = CGImageMetadataCreateXMPData(metadata, nil)
let xmp = String(data: xmpData as! Data, encoding: .utf8)!
print(xmp)
Copy the code
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:apple_desktop="http://ns.apple.com/namespace/1.0/">
         <apple_desktop:solar>
            <! -- (Base64-Encoded Metadata) -->
        </apple_desktop:solar>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>
Copy the code

Oh.

But it’s a good thing we checked. To successfully customize Dynamic Desktop, you must also rely on the apple_desktop namespace.

In that case, let’s get started.

Create a custom Dynamic Desktop

First, create a data model to represent the Dynamic Desktop:

struct DynamicDesktop {
    let images: [Image]

    struct Image {
        let cgImage: CGImage
        let metadata: Metadata

        struct Metadata: Codable {
            let index: Int
            let altitude: Double
            let azimuth: Double

            private enum CodingKeys: String.CodingKey {
                case index = "i"
                case altitude = "a"
                case azimuth = "z"}}}}Copy the code

As mentioned earlier, each Dynamic Desktop consists of an ordered sequence of images, each of which contains image data and metadata stored in CGImage objects. Metadata is of the Codable type for the compiler to synthesize related functions automatically. We can feel its advantages when generating a base64-encoded binary attribute list.

Write image target

First, create a CGImageDestination that specifies the output URL. The file type is HEIC, and the number of resources is the number of images to be included.

guard let imageDestination = CGImageDestinationCreateWithURL(
                                outputURL as CFURL.AVFileType.heic as CFString,
                                dynamicDesktop.images.count.nil
                             )
else {
    fatalError("Error creating image destination")}Copy the code

Next, iterate over all the images in the dynamic desktop object. We can also get the current index by using the enumerated() method, so we can set the image metadata on the first image:

for (index, image) in dynamicDesktop.images.enumerated() {
    if index == 0 {
        let imageMetadata = CGImageMetadataCreateMutable(a)guard let tag = CGImageMetadataTagCreate(
                            "http://ns.apple.com/namespace/1.0/" as CFString."apple_desktop" as CFString."solar" as CFString,
                            .string,
                            try! dynamicDesktop.base64EncodedMetadata() as CFString
                        ),
            CGImageMetadataSetTagWithPath(
                imageMetadata, nil."xmp:solar" as CFString, tag
            )
        else {
            fatalError("Error creating image metadata")}CGImageDestinationAddImageAndMetadata(imageDestination,
                                              image.cgImage,
                                              imageMetadata,
                                              nil)}else {
        CGImageDestinationAddImage(imageDestination,
                                   image.cgImage,
                                   nil)}}Copy the code

Aside from the more verbose Core Graphics API, the code is pretty straightforward. The only one that needs further explanation is CGImageMetadataTagCreate(_:_:_:_: :).

Because the image and metadata container structures and code representations are different, we had to implement the Encodable protocol for DynamicDesktop:

extension DynamicDesktop: Encodable {
    private enum CodingKeys: String.CodingKey {
        case ap, si
    }

    private enum NestedCodingKeys: String.CodingKey {
        case d, l
    }

    func encode(to encoder: Encoder) throws {
        var keyedContainer =
            encoder.container(keyedBy: CodingKeys.self)

        var nestedKeyedContainer =
            keyedContainer.nestedContainer(keyedBy: NestedCodingKeys.self,
                                           forKey: .ap)

        // FIXME: not sure what 'l' and 'd' mean here
        try nestedKeyedContainer.encode(0, forKey: .l)
        try nestedKeyedContainer.encode(self.images.count, forKey: .d)

        var unkeyedContainer =
            keyedContainer.nestedUnkeyedContainer(forKey: .si)
        for image in self.images {
            try unkeyedContainer.encode(image.metadata)
        }
    }
}
Copy the code

With this, you can implement the base64EncodedMetadata() method mentioned in the previous code:

extension DynamicDesktop {
    func base64EncodedMetadata(a) throws -> String {
        let encoder = PropertyListEncoder()
        encoder.outputFormat = .binary

        let binaryPropertyListData = try encoder.encode(self)
        return binaryPropertyListData.base64EncodedString()
    }
}
Copy the code

When the for – performed in cycle, it suggests that all images and metadata to be written, we can call CGImageDestinationFinalize (_) method to terminate the source images, pictures and will be written to disk.

guard CGImageDestinationFinalize(imageDestination) else {
    fatalError("Error finalizing image")}Copy the code

If all goes well, you can be proud of redefining yourself for Dynamic Desktop. Great!




We love Mojave’s Dynamic Desktop feature, and are pleased to see that it seems to mirror the success of Windows 95 wallpapers when they hit the mainstream.

If you feel the same way, here are some ideas to consider:

Photos automatically generate Dynamic Desktop

It’s exciting that such a lofty study of celestial motion can be expressed simply as a binary equation: time and position.

In the previous example, this information is hard-coded, but it can be automatically retrieved by reading the image data.

By default, most phone cameras capture Exif metadata at the time of shooting. The metadata includes when the photo was taken and the GPS coordinates of the device at the time.

Automatically get the position of the sun by reading the time and position information in the metadata, so it makes sense to generate the Dynamic Desktop from a series of images.

Time-lapse photography on the iPhone

Want to put your new iPhone Xs to good use? (More to the point, “Do something creative with your old iPhone while trying to decide whether to sell it?” )

Charge your phone, place it in front of your window, turn on your camera’s time-lapse mode, and click the “Take” button. Select a few keyframes from the final video and make your own Dynamic Desktop.

Of course, you can look at apps like Skyflow, which allows you to take still images at intervals.

Create landscapes through GIS data

If you can’t bear to be without your phone all day (sad), or there aren’t any landmarks worth photographing (still sad), you can create a world of your own (which is sadder than reality itself).

Use an app like Terragen, which creates a realistic 3D world with fine tuning of the sun, earth and sky.

For further simplification, you can also download elevation maps from the USGS national Map site to use as templates for 3D rendering.

Download the prefabricated Dynamic Desktops

Or, if you have too much work to do and don’t have the time to make nice pictures, you can buy from someone else for a fee.

I’m personally a fan of the 24 Hour Wallpaper app. If you have other recommendations, please feel free to contact us.



NSMUTABLEHIPSTER

Is it? Error correction? Welcome to Issues and pull Requests — NSHipster is better because of you.

Swift 4.2 is used for this article. For information about the status of articles on the site, see the status summary page.

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.