macOS 動態桌面

做者: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。

利用 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 鍵對應的值是包含 dl 兩個鍵的字典,它們的值都是整型。

si 鍵對應的值是包含多個字典的數組,字典中有整型,也有浮點型的值。在嵌套的字典中,i 最容易理解:它從 0 一直遞增到 15,這表示的是圖片序列的下標。在沒有更多信息的狀況下,很難猜想 az 的含義,其實它們表示相應圖片中太陽的高度(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

疑問?糾錯?歡迎提 issuespull requests —— NSHipster 因你而變得更好。

本文用的是 Swift 4.2。關於站內文章的狀態信息,能夠查看 狀態彙總頁面

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg

相關文章
相關標籤/搜索