作者: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)。更多資訊,可以參考 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 這款應用的粉絲。如果你有別的推薦,歡迎 聯絡我們。
NSMUTABLEHIPSTER
疑問?糾錯?歡迎提 issues 和 pull requests —— NSHipster 因你而變得更好。
本文用的是 Swift 4.2。關於站內文章的狀態資訊,可以檢視 狀態彙總頁面。
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg。