做者:Mattt,原文連接,原文日期:2018-10-01 譯者:saitjr;校對:冬瓜,Yousanflics;定稿:Forelaxgit
Dark Mode(深色模式)可謂是 macOS 最受歡迎的特性之一了 —— 尤爲是對於你我這樣的開發者來講。咱們不只喜歡文本編輯器是暗色的主題,還很看中整個系統色調的一致性。github
過去幾年,和這個特性旗鼓至關的要數 Night Shift(夜覽),它主要是在日夜更替的時候減小對眼睛的勞損。macos
縱觀這兩個功能,Dynamic Desktop(動態桌面)也就呼之欲出了,固然這也是 Mojave 的新特性之一。進入「系統偏好設置 > 桌面與屏幕保護程序」 而且選擇「動態」,就能獲得一個基於地理位置且全天候動態變化的壁紙。swift
效果不只微妙,並且讓人愉悅。桌面彷彿被賦予了生命,能隨着時間的推移而變化;符合天然規律。(不出意外的話,結合 dark mode 的切換,還會有討喜的特效)數組
這究竟是如何實現的呢?
這即是本週 NSHipster 討論的問題。bash
答案會深刻探究圖片格式,同時涉及一些逆向工程以及球面三角學相關的內容。app
理解 Dynamic Desktop 第一步,就是要找到這些動態圖片。編輯器
在 macOS Mojave 系統下,打開訪達,選擇「前往 > 前往文件夾...」 (⇧⌘G),輸入「/Library/Desktop Pictures/」。ide
在這個目錄下,能夠找到名爲「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。
第一步先建立 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 的「太陽錶盤」。
好吧,天文學到此結束。接下來是一個乏味的過程:擺在眼前的 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:
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 壁紙進入主流市場時的輝煌。
若是你也這樣想,下面還有些想法可供參考:
讓人振奮的是,天體運動這樣遙不可及的研究,居然能夠簡化用二元方程來表達:時間與位置。
在以前的例子中,這部分信息都是硬編碼的,但其實它們能夠經過讀取圖片數據來自動獲取。
默認狀況下,絕大部分手機的相機都會捕獲拍攝時的 Exif 元數據。元數據包含了照片拍攝的時間,以及當時設備的 GPS 座標。
經過讀取元數據中的時間與位置信息,能自動獲取太陽的位置,那麼從一系列圖片中生成 Dynamic Desktop 也就瓜熟蒂落了。
想要好好利用手上全新的 iPhone Xs 嗎?(更確切的說,「在糾結賣不賣舊 iPhone 的時候,能夠先用它來作些有創意的事?」)
將手機充上電,擺在窗前,打開相機的延時攝影模式,點擊「拍攝」按鈕。從最後的視頻中選出一些關鍵幀,就能夠製做專屬 Dynamic Desktop 了。
固然,你能夠看看 Skyflow 這類應用,它能設置時間間隔來拍攝靜態圖片。
若是你沒法忍受手機一成天不在身邊(傷心),又或者沒什麼標誌性景象值得拍攝(依然傷心),你還能夠創造一個屬於本身的世界(這比現實自己還要使人傷心)。
能夠選擇用 Terragen 這類應用,它打造了一個逼真的 3D 世界,還能對太陽、地球、天空進行微調。
想要更加簡化,還能夠從美國地質調查局的 國家地圖網站 上下載高程地圖,以用於 3D 渲染的模板。
再或者,你天天都很是多的工做要作,抽不出時間搗騰好看的圖片,也能夠選擇付費從別人那裏購買。
我我的是 24 Hour Wallpaper 這款應用的粉絲。若是你有別的推薦,歡迎 聯繫咱們。
疑問?糾錯?歡迎提 issues 和 pull requests —— NSHipster 因你而變得更好。
本文用的是 Swift 4.2。關於站內文章的狀態信息,能夠查看 狀態彙總頁面。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg。