Skip to content

Instantly share code, notes, and snippets.

@ole
Last active March 8, 2024 02:17
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ole/6b6b5ef20fbec12e9227075e20c6e6ef to your computer and use it in GitHub Desktop.
Save ole/6b6b5ef20fbec12e9227075e20c6e6ef to your computer and use it in GitHub Desktop.
Reverse-engineering the dynamic wallpaper file format in macOS Mojave.

The dynamic wallpaper in MacOS Mojave is a single 114 MB .heic file that seems to contain 16 embedded images.

It also contains the following binary plist data in its metadata under the key "Solar". It's an array of 16 items, each with four keys:

  • i (integer). This seems to be the image index.
  • o (integer). This is always 1 or 0. Stephen Radford thinks it indicates dark mode (0) vs. light mode (1).
  • a (decimal). I’m pretty sure this is the angle of the sun over the horizon. 0º = sunset/sunrise. 90º = sun directly overhead. Negative values = sun below horizon.
  • z (decimal). This seems to be the cardinal position of the sun relative to the camera. 0º = sun is directly in front of the camera. 90º = sun is directly to the right of the camera. 180º = sun is directly behind the camera.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>si</key>
<array>
<dict>
<key>i</key>
<integer>0</integer>
<key>z</key>
<real>270.9334057827345</real>
<key>a</key>
<real>-0.3427528387535028</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>1</integer>
<key>z</key>
<real>81.77588714480999</real>
<key>a</key>
<real>-10.23975864472505</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>2</integer>
<key>z</key>
<real>86.33545030477751</real>
<key>a</key>
<real>-4.247734408075456</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>3</integer>
<key>z</key>
<real>90.81267037496195</real>
<key>a</key>
<real>1.389086633100843</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>4</integer>
<key>z</key>
<real>95.30740958876589</real>
<key>a</key>
<real>7.167168970526129</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>5</integer>
<key>z</key>
<real>99.92062963268938</real>
<key>a</key>
<real>13.08619419164163</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>6</integer>
<key>z</key>
<real>129.1865220819196</real>
<key>a</key>
<real>40.41563946490428</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>7</integer>
<key>z</key>
<real>182.2330942549791</real>
<key>a</key>
<real>53.43347266172774</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>8</integer>
<key>z</key>
<real>233.5515919580959</real>
<key>a</key>
<real>38.79312820063863</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>9</integer>
<key>z</key>
<real>261.8715904657666</real>
<key>a</key>
<real>11.08942317126588</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>10</integer>
<key>z</key>
<real>266.4432737071051</real>
<key>a</key>
<real>5.184575323673625</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>11</integer>
<key>z</key>
<real>275.4420453669525</real>
<key>a</key>
<real>-6.248309374122789</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>12</integer>
<key>z</key>
<real>280.0703158940117</real>
<key>a</key>
<real>-12.20770735214888</real>
<key>o</key>
<integer>1</integer>
</dict>
<dict>
<key>i</key>
<integer>13</integer>
<key>z</key>
<real>309.4185731874514</real>
<key>a</key>
<real>-39.48933951993012</real>
<key>o</key>
<integer>0</integer>
</dict>
<dict>
<key>i</key>
<integer>14</integer>
<key>z</key>
<real>2.175096553867547</real>
<key>a</key>
<real>-52.75318137879935</real>
<key>o</key>
<integer>0</integer>
</dict>
<dict>
<key>i</key>
<integer>15</integer>
<key>z</key>
<real>53.50908581251309</real>
<key>a</key>
<real>-38.04743388682423</real>
<key>o</key>
<integer>0</integer>
</dict>
</array>
</dict>
</plist>
@tim-hilt
Copy link

tim-hilt commented Nov 3, 2021

Can you tell me how you got the plist-output? I mainly use Linux and would love to build a small utility that acts like the MacOS dynamic background. There are no such utilities yet that incorporate solar positioning.

@ole
Copy link
Author

ole commented Nov 4, 2021

@tim-hilt Paste the following code into an Xcode playground or turn it into a command-line script, this should get your started. Note that the code will not work on Linux since it uses Apple-proprietary APIs to parse the image's metadata. If you want to use another library for this, look for the apple_desktop:solar key in the image's XMP metadata.

// Extract and decode the apple_desktop:solar plist embedded in macOS Dynamic Wallpaper images.

import Foundation
import ImageIO

let imageFileURL = URL(fileURLWithPath: "/System/Library/Desktop Pictures/Monterey Graphic.heic")
let imageSource = CGImageSourceCreateWithURL(imageFileURL as CFURL, nil)!
let metadata = imageSource.metadataForImage(at: 0)!
let metadataTags = metadata.tags
let appleDesktopSolarTag = metadataTags.first { tag in
  tag.prefix == "apple_desktop" && tag.name == "solar"
}!
let binaryPlistBase64 = appleDesktopSolarTag.value as! String

let binaryPlistData = Data(base64Encoded: binaryPlistBase64)!
let plistContents = try PropertyListSerialization.propertyList(from: binaryPlistData, format: nil)
print(plistContents)

extension CGImageSource {
  var status: CGImageSourceStatus {
    return CGImageSourceGetStatus(self)
  }

  var uti: String? {
    return CGImageSourceGetType(self) as String?
  }

  var count: Int {
    return CGImageSourceGetCount(self)
  }

  var properties: [String: Any] {
    return CGImageSourceCopyProperties(self, nil) as? [String: Any] ?? [:]
  }

  func propertiesForImage(at index: Int) -> [String: Any] {
    return CGImageSourceCopyPropertiesAtIndex(self, index, nil) as? [String: Any] ?? [:]
  }

  func metadataForImage(at index: Int) -> CGImageMetadata? {
    return CGImageSourceCopyMetadataAtIndex(self, index, nil)
  }
}

extension CGImageMetadata {
  var tags: [CGImageMetadataTag] {
    return (CGImageMetadataCopyTags(self) as? [CGImageMetadataTag]) ?? []
  }
}

extension CGImageMetadataTag {
  var namespace: String? {
    return CGImageMetadataTagCopyNamespace(self) as String?
  }

  var prefix: String? {
    return CGImageMetadataTagCopyPrefix(self) as String?
  }

  var name: String? {
    return CGImageMetadataTagCopyName(self) as String?
  }

  var value: Any? {
    return CGImageMetadataTagCopyValue(self)
  }

  var type: CGImageMetadataType {
    return CGImageMetadataTagGetType(self)
  }
}

extension CGImageSourceStatus: CustomStringConvertible {
  public var description: String {
    switch self {
    case .statusUnexpectedEOF: return "statusUnexpectedEOF"
    case .statusInvalidData: return "statusInvalidData"
    case .statusUnknownType: return "statusUnknownType"
    case .statusReadingHeader: return "statusReadingHeader"
    case .statusIncomplete: return "statusIncomplete"
    case .statusComplete: return "statusComplete"
    @unknown default: return "unknown"
    }
  }
}

extension CGImageMetadataType: CustomStringConvertible {
  public var description: String {
    switch self {
    case .invalid: return "invalid"
    case .default: return "default"
    case .string: return "string"
    case .arrayUnordered: return "arrayUnordered"
    case .arrayOrdered: return "arrayOrdered"
    case .alternateArray: return "alternateArray"
    case .alternateText: return "alternateText"
    case .structure: return "structure"
    @unknown default: return "unknown"
    }
  }
}

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