mac OS 動態桌面
作者:Mattt,原文連結,原文日期:2018-10-01 譯者:saitjr;校對:冬瓜,Yousanflics;定稿:Forelax
Dark Mode(深色模式)可謂是 macOS 最受歡迎的特性之一了 —— 尤其是對於你我這樣的開發者來說。我們不僅喜歡文字編輯器是暗色的主題,還很看中整個系統色調的一致性。
過去幾年,和這個特性旗鼓相當的要數 Night Shift(夜覽),它主要是在日夜更替的時候減少對眼睛的勞損。
縱觀這兩個功能,Dynamic Desktop(動態桌面)也就呼之欲出了,當然這也是 Mojave 的新特性之一。進入“系統偏好設定 > 桌面與螢幕保護程式” 並且選擇“動態”,就能得到一個基於地理位置且全天候動態變化的桌布。

效果不僅微妙,而且讓人愉悅。桌面彷彿被賦予了生命,能隨著時間的推移而變化;符合自然規律。(不出意外的話,結合 dark mode 的切換,還會有討喜的特效)
這到底是如何實現的呢?
這便是本週 NSHipster 討論的問題。
答案會深入探究圖片格式,同時涉及一些逆向工程以及球面三角學相關的內容。
理解 Dynamic Desktop 第一步,就是要找到這些動態圖片。
在 macOS Mojave 系統下,開啟訪達,選擇“前往 > 前往資料夾...” (⇧⌘G),輸入“/Library/Desktop Pictures/”。

在這個目錄下,可以找到名為“Mojave.heic”的檔案。雙擊通過預覽開啟。

在預覽中,左邊欄會顯示從 1~16 的縮圖,每張都是不同狀態的沙漠圖。

如果選擇“工具 > 顯示檢查器”(⌘I),可以看到更為詳細的資訊,如下圖所示:

不幸的是,這些就是預覽所展示的全部資訊了(截至發稿前)。即使點選旁邊的“更多資訊檢查器”,我們也只是能得到下面這個表格,其餘的無從得知:
Color Model | RGB |
---|---|
Depth: | 8 |
Pixel Height | 2,880 |
Pixel Width | 5,120 |
Profile Name | Display P3 |
字尾 .heic
表示圖片容器採用 HFIF(High-Efficiency Image File Format)編碼,即高效率圖檔格式(這種格式基於 HEVC (High-Efficiency Video Compression),即高效率視訊壓縮,也就是 H.265)。更多資訊,可以參考 ofollow,noindex">WWDC 2017 Session 503 "Introducing HEIF and HEVC"
想要獲得更多的資料,我們還需要腳踏實地,真真切切的深入底層 API。
利用 CoreGraphics 一探究竟
第一步先建立 Xcode Playground。簡單起見,我們將“Mojave.heic”檔案路徑硬編碼到程式碼中。
import Foundation import CoreGraphics // 系統版本要求 macOS 10.14 Mojave let url = URL(fileURLWithPath: "/Library/Desktop Pictures/Mojave.heic") 複製程式碼
然後,建立 CGImageSource
,拷貝元資料並遍歷全部標籤:
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) } 複製程式碼
執行這段程式碼,會得到兩個值:一個是 hasXMP
,值為 "True"
,另一個是 solar
,它的值是一串看不大懂的資料:
YnBsaXN0MDDRAQJSc2mvEBADDBAUGBwgJCgsMDQ4PEFF1AQFBgcICQoLUWlRelFh UW8QACNAcO7vOubr3yO/1e+pmkOtXBAB1AQFBgcNDg8LEAEjQFRxqCKOFiAjwCR6 waUkDgHUBAUGBxESEwsQAiNAVZV4BI4c+CPAEP2uFrMcrdQEBQYHFRYXCxADI0BW tALKmrjwIz/2ObLnx6l21AQFBgcZGhsLEAQjQFfTrJlEjnwjQByrLle1Q0rUBAUG Bx0eHwsQBSNAWPrrmI0ISCNAKiwhpSRpc9QEBQYHISIjCxAGI0BgJff9KDpyI0BE NTOsilht1AQFBgclJicLEAcjQGbHdYIVQKojQEq3fAg86lXUBAUGBykqKwsQCCNA bTGmpC2YRiNAQ2WFOZGjntQEBQYHLS4vCxAJI0BwXfII2B+SI0AmLcjfuC7g1AQF BgcxMjMLEAojQHCnF6YrsxcjQBS9AVBLTq3UBAUGBzU2NwsQCyNAcTcSnimmjCPA GP5E0ASXJtQEBQYHOTo7CxAMI0BxgSADjxK2I8AoalieOTyE1AQFBgc9Pj9AEA0j QHNWsnnMcWIjwEO+oq1pXr8QANQEBQYHQkNEQBAOI0ABZpkFpAcAI8BKYGg/VvMf 1AQFBgdGR0hAEA8jQErBKblRzPgjwEMGElBIUO0ACAALAA4AIQAqACwALgAwADIA NAA9AEYASABRAFMAXABlAG4AcAB5AIIAiwCNAJYAnwCoAKoAswC8AMUAxwDQANkA 4gDkAO0A9gD/AQEBCgETARwBHgEnATABOQE7AUQBTQFWAVgBYQFqAXMBdQF+AYcB kAGSAZsBpAGtAa8BuAHBAcMBzAHOAdcB4AHpAesB9AAAAAAAAAIBAAAAAAAAAEkA AAAAAAAAAAAAAAAAAAH9 複製程式碼
太陽之光
大多數人看到這串文字,就會默默合上 MacBook Pro,大呼告辭。但一定有人發現,這串文字非常像Base64 編碼 的傑作。
讓我們來驗證一下這個假設:
if name == "solar" { let data = Data(base64Encoded: value)! print(String(data: data, encoding: .ascii)) } 複製程式碼
bplist00Ò\u{01}\u{02}\u{03}...
這又是什麼? bplist
後面接了一串亂碼?
天哪,原來這是二進位制屬性列表 的檔案簽名。
利用 PropertyListSerialization
來看看呢...
if name == "solar" { let data = Data(base64Encoded: value)! let propertyList = try PropertyListSerialization .propertyList(from: data, options: [], format: nil) print(propertyList) } 複製程式碼
( ap = { d = 15; l = 0; }; si = ( { a = "-0.3427528387535028"; i = 0; z = "270.9334057827345"; }, ... { a = "-38.04743388682423"; i = 15; z = "53.50908581251309"; } ) ) 複製程式碼
清晰多了!
首先有兩個一級鍵:
ap
鍵對應的值是包含 d
和 l
兩個鍵的字典,它們的值都是整型。
si
鍵對應的值是包含多個字典的陣列,字典中有整型,也有浮點型的值。在巢狀的字典中, i
最容易理解:它從 0 一直遞增到 15,這表示的是圖片序列的下標。在沒有更多資訊的情況下,很難猜測 a
與 z
的含義,其實它們表示相應圖片中太陽的高度( a
)和方位角( z
)。
計算太陽的位置
就在我落筆之時,身處北半球的人正在進入秋季,白晝變短,氣溫變低,而南半球的人卻經歷著白晝變長,氣溫變高。季節的變化告訴我們,日照的時長取決於你在星球上的位置,以及星球繞太陽的軌道。
可喜的是,天文學家能告訴你 —— 而且相當準確 —— 太陽在天空中的位置或時間。不可賀的是,這其中的計算十分複雜。
但老實講,我們並不用過分深究它,在網上能找到相關的程式碼。經過不斷的試錯, 它們就能為我所用 (歡迎 PR!):
import Foundation import CoreLocation // 位於加州庫比蒂諾的 Apple Park let location = CLLocation(latitude: 37.3327, longitude: -122.0053) let time = Date() 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") 複製程式碼
Solar Position on Oct 1, 2018 at 12:00 180.73470025840783° Az / 49.27482549913847° El
2018 年 10 月 1 日中午,太陽從南面照射在 Apple Park,大約處於地平線中間,直射頭頂。
如果繪製出太陽一天的位置,我們可以得到一個正弦曲線,這不禁讓人聯想到 Apple Watch 的“太陽錶盤”。

擴充套件對 XMP 的理解
好吧,天文學到此結束。接下來是一個乏味的過程: 擺在眼前 的 XML 元資料。
還記得之前的元資料鍵 hasXMP
嗎?對,就是它沒錯。
XMP(Extensible Metadata Platform),即可擴充套件元資料平臺,是一種使用元資料標記檔案的標準格式。XMP 長什麼樣呢?請打起精神來:
let xmpData = CGImageMetadataCreateXMPData(metadata, nil) let xmp = String(data: xmpData as! Data, encoding: .utf8)! print(xmp) 複製程式碼
<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> 複製程式碼
嘔。
不過也幸好我們檢查了一下。之後想要成功自定義 Dynamic Desktop,還得仰仗 apple_desktop
名稱空間。
既然如此,就開始吧。
建立自定義 Dynamic Desktop
首先,建立一個數據模型來表示 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" } } } } 複製程式碼
如前文所述,每個 Dynamic Desktop 都由一個有序的圖片序列構成,每個圖片又包含儲存在 CGImage
物件中的圖片資料和元資料。 Metadata
採用 Codable
型別,是為了編譯器自動合成相關函式。我們能在生成 Base64 編碼的二進位制屬性列表時感受到它的優勢。
寫入圖片目標
首先,建立一個指定輸出 URL 的 CGImageDestination
。檔案型別為 heic
,資源數量即需要包含的圖片張數。
guard let imageDestination = CGImageDestinationCreateWithURL( outputURL as CFURL, AVFileType.heic as CFString, dynamicDesktop.images.count, nil ) else { fatalError("Error creating image destination") } 複製程式碼
接著,遍歷動態桌面物件中的全部圖片。通過 enumerated()
方法,我們還能獲取到當前 index
,這樣就可以在第一張圖片上設定圖片元資料:
for (index, image) in dynamicDesktop.images.enumerated() { if index == 0 { let imageMetadata = CGImageMetadataCreateMutable() 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) } } 複製程式碼
除了較為繁雜的 Core Graphics API 以外,程式碼可以說非常直觀了。唯一需要進一步解釋的只有 CGImageMetadataTagCreate(_:_:_:_:_:)
。
由於圖片與元資料容器的結構、程式碼的表現形式均不同,所以我們不得不為 DynamicDesktop
實現 Encodable
協議:
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:不確定此處 `l` 與 `d` 的含義 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) } } } 複製程式碼
有了這個,就可以實現之前程式碼中提到的 base64EncodedMetadata()
方法了:
extension DynamicDesktop { func base64EncodedMetadata() throws -> String { let encoder = PropertyListEncoder() encoder.outputFormat = .binary let binaryPropertyListData = try encoder.encode(self) return binaryPropertyListData.base64EncodedString() } } 複製程式碼
當 for-in 迴圈執行完,也就表明所有圖片和元資料均被寫入,我們可以呼叫 CGImageDestinationFinalize(_:)
方法終止圖片源,並將圖片寫入磁碟。
guard CGImageDestinationFinalize(imageDestination) else { fatalError("Error finalizing image") } 複製程式碼
如果一切順利,就可以為重新定義 Dynamic Desktop 的自己而感到驕傲了。棒!
我們非常喜歡 Mojave 的 Dynamic Desktop 特性,並且也很欣慰看到它彷彿重現了 Windows 95 桌布進入主流市場時的輝煌。
如果你也這樣想,下面還有些想法可供參考:
照片自動生成 Dynamic Desktop
讓人振奮的是,天體運動這樣高不可攀的研究,竟然可以簡化用二元方程來表達:時間與位置。
在之前的例子中,這部分資訊都是硬編碼的,但其實它們可以通過讀取圖片資料來自動獲取。
預設情況下,絕大部分手機的相機都會捕獲拍攝時的Exif 元資料。元資料包含了照片拍攝的時間,以及當時裝置的 GPS 座標。
通過讀取元資料中的時間與位置資訊,能自動獲取太陽的位置,那麼從一系列圖片中生成 Dynamic Desktop 也就順理成章了。
iPhone 上的延時攝影
想要好好利用手上全新的 iPhone Xs 嗎?(更確切的說,“在糾結賣不賣舊 iPhone 的時候,可以先用它來做些有創意的事?”)
將手機充上電,擺在窗前,開啟相機的延時攝影模式,點選“拍攝”按鈕。從最後的視訊中選出一些關鍵幀,就可以製作專屬 Dynamic Desktop 了。
當然,你可以看看Skyflow 這類應用,它能設定時間間隔來拍攝靜態圖片。
通過 GIS 資料打造風景
如果你無法忍受手機一整天不在身邊(傷心),又或者沒什麼標誌性景象值得拍攝(依然傷心),你還可以創造一個屬於自己的世界(這比現實本身還要令人傷心)。
可以選擇用Terragen 這類應用,它打造了一個逼真的 3D 世界,還能對太陽、地球、天空進行微調。
想要更加簡化,還可以從美國地質調查局的國家地圖網站 上下載高程地圖,以用於 3D 渲染的模板。
下載預製的 Dynamic Desktops
再或者,你每天都非常多的工作要做,抽不出時間搗騰好看的圖片,也可以選擇付費從別人那裡購買。
我個人是24 Hour Wallpaper 這款應用的粉絲。如果你有別的推薦,歡迎聯絡我們。