meta-data 标签动态修改_macOS 动态桌面(系统型自带)

本文探讨了如何在macOS中动态修改meta-data标签以创建自定义Dynamic Desktop。从理解动态壁纸的工作原理开始,通过CoreGraphics API深入研究图片元数据,特别是XMP(可扩展元数据平台),并介绍了计算太阳位置的相关知识。文章提供了一个数据模型,展示了如何创建和写入图片目标,最终实现动态桌面的自定义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

30e5c1843548e3789e784890a524cf0f.png

Dark Mode(深色模式)可谓是 macOS 最受欢迎的特性之一了 —— 尤其是对于你我这样的开发者来说。我们不仅喜欢文本编辑器是暗色的主题,还很看中整个系统色调的一致性。

过去几年,和这个特性旗鼓相当的要数 Night Shift(夜览),它主要是在日夜更替的时候减少对眼睛的劳损。

纵观这两个功能,Dynamic Desktop(动态桌面)也就呼之欲出了,当然这也是 Mojave 的新特性之一。进入“系统偏好设置 > 桌面与屏幕保护程序” 并且选择“动态”,就能得到一个基于地理位置且全天候动态变化的壁纸。

效果不仅微妙,而且让人愉悦。桌面仿佛被赋予了生命,能随着时间的推移而变化;符合自然规律。(不出意外的话,结合 dark mode 的切换,还会有讨喜的特效)

答案会深入探究图片格式,同时涉及一些逆向工程以及球面三角学相关的内容。

(测试环境为Mojave。)


理解 Dynamic Desktop 第一步,就是要找到这些动态图片。

在 macOS Mojave 系统下,打开访达,选择“前往 > 前往文件夹...” (⇧⌘G),输入“/Library/Desktop Pictures/”。

在这个目录下,可以找到名为“Mojave.heic”的文件。双击通过预览打开。

在预览中,左边栏会显示从 1~16 的缩略图,每张都是不同状态的沙漠图。

如果选择“工具 > 显示检查器”(⌘I),可以看到更为详细的信息,如下图所示:

不幸的是,这些就是预览所展示的全部信息了(截至发稿前)。即使点击旁边的“更多信息检查器”,我们也只是能得到下面这个表格,其余的无从得知:

d052a3b117cfc26c9ef4b3af655bdaba.png
预览所展示的全部信息
后缀.heic表示图片容器采用 HFIF(High-Efficiency Image File Format)编码,即高效率图档格式(这种格式基于 HEVC(High-Efficiency Video Compression),即高效率视频压缩,也就是 H.265)。

想要获得更多的数据,我们还需要脚踏实地,真真切切的深入底层 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值