macOS Dynamic Desktop
다크 모드는 macOS Mojave의 가장 유명한 기능 중 하나입니다. 특히 우리 개발자들에게는요.
몇 년 전엔 어떤 팬이 Night Shift와 비슷하고 늦은 밤에 (또는 이른 아침) 개발하는 사람들의 눈의 피로를 줄여주는 기능을 만들기도 했습니다.
“시스템 환경설정 > 데스크탑 및 화면 보호기”에 가면 새로운 선택지인 “다이내믹”을 볼 수 있을 것입니다. 이 기능은 여러분의 위치와 시간에 따라 배경화면이 변하는 기능입니다.
결과는 감지하기 힘들지만 시간이 지나서 확인해보면 기분이 좋아집니다. 시간에 따라 배경화면을 보고있으면 자연과 조화돼서 마치 배경화면이 살아있는 것 같습니다.
내부에선 정확히 어떤 일이 일어나는걸까요?
그게 바로 이번주의 NSHipster의 주제입니다.
그에 대한 대답을 하기 위해 우리는 이미지 포맷을 깊숙히 파보며 약간의 리버스 엔지니어링과 구형 삼각법도 알아볼 것입니다.
Dynamic Desktop이 어떻게 작동하는지 이해하기 위한 가장 첫 번째는 다이내믹 이미지를 알아보는 것입니다.
macOS Mojave를 설치하셨다면 파인더를 열고 “이동 > 폴더로 이동”(⇧⌘G)을 선택하시고 “/Library/Desktop Pictures/”를 입력하세요.
이 폴더안에 있는 “Mojave.heic” 파일을 찾으시고 미리보기 앱으로 열어보세요.
미리보기의 사이드 바를 보면 각자 사막의 다른 화면을 나타내고 있는 16개의 썸네일을 확인할 수 있을 것입니다.
이제 “도구 > 속성 보기” (⌘I) 를 선택하면 우리가 찾던 일반적인 정보를 볼 수 있습니다.
아쉽게도 이게 미리보기가 우리에게 제공할 수 있는 모든 정보입니다. (지금 이 글을 적고있는 지금까지는요.) 다음 패널인 “추가 정보”를 선택하면 우리의 주제에 대한 더 많은 정보를 얻을 수 있습니다.
Color Model (색상 모델) | RGB |
Depth (심도) | 8 |
Pixel Height (픽셀 높이) | 2,880 |
Pixel Width (픽셀 너비) | 5,120 |
Profile Name (프로파일 이름) | Display P3 |
우리는 더 자세히 알고싶기 때문에 소매를 걷고 더 낮은 단계의 API로 들어가봅시다.
CoreGraphics로 더 자세히 파헤치기
새로운 Xcode Playground를 생성해서 조사를 시작해보겠습니다. 간편함을 위해 시스템의 “Mojave.heic” 파일의 URL을 하드코딩하겠습니다.
import Foundation
import Core Graphics
// mac OS 10.14 Mojave가 필요합니다
let url = URL(file URLWith Path: "/Library/Desktop Pictures/Mojave.heic")
다음은 CGImage
를 만들고 메타데이터를 복사해서 모든 태그들을 둘러보겠습니다.
let source = CGImage Source Create With URL(url as CFURL, nil)!
let metadata = CGImage Source Copy Metadata At Index(source, 0, nil)!
let tags = CGImage Metadata Copy Tags(metadata) as! [CGImage Metadata Tag]
for tag in tags {
guard let name = CGImage Metadata Tag Copy Name(tag),
let value = CGImage Metadata Tag Copy Value(tag)
else {
continue
}
print(name, value)
}
위의 코드를 실행하면 두 가지 결과를 얻을 수 있습니다. "True"
값을 가지는 has
와 조금은 이해하기 힘든 값을 가지는 solar
입니다.
Yn Bsa XN0MDDRAQJSc2mv EBADDBAUGBwg JCgs MDQ4PEFF1AQFBgc ICQo LUWl Rel Fh
UW8QACNAc O7v Oubr3y O/1e+pmk Ot XBAB1AQFBgc NDg8LEAEj QFRxq CKOFi Ajw CR6
wa Uk Dg HUBAUGBx ESEws QAi NAVZV4BI4c+CPAEP2u Fr Mcrd QEBQYHFRYXCx ADI0BW
t ALKmrjw Iz/2Ob Lnx6l21AQFBgc ZGhs LEAQj QFf Tr Jl Ejnwj QByr Lle1Q0r UBAUG
Bx0e Hws QBSNAWPrrm I0ISCNAKiwhp SRpc9QEBQYHISIj Cx AGI0Bg Jff9KDpy I0BE
NTOsilht1AQFBgcl Jic LEAcj QGb Hd YIVQKoj QEq3f Ag86l XUBAUGBykq Kws QCCNA
b TGmp C2YRi NAQ2WFOZGjnt QEBQYHLS4v Cx AJI0Bw Xf II2B+SI0Am Lcjfu C7g1AQF
Bgcx Mj MLEAoj QHCn F6Yrsxcj QBS9AVBLTq3UBAUGBz U2Nws QCy NAc Tc Snimmj CPA
GP5E0ASXJt QEBQYHOTo7Cx AMI0Bxg SADjx K2I8Aoalie OTy E1AQFBgc9Pj9AEA0j
QHNWsnn Mc WIjw EO+oq1p Xr8QANQEBQYHQk NEQBAOI0ABZpk Fp Ac AI8BKYGg/Vv Mf
1AQFBgd GR0h AEA8j QEr BKbl Rz Pgjw EMGEl BIUO0ACAALAA4AIQAq ACw ALg Aw ADIA
NAA9AEYASABRAFMAXABl AG4Ac AB5AIIAiw CNAJYAnw Co AKo Asw C8AMUAxw DQANk A
4g Dk AO0A9g D/AQEBCg ETARw BHg En ATABOQE7AUQBTQFWAVg BYQFq AXMBd QF+AYc B
k AGSAZs Bp AGt Aa8Bu AHBAc MBz AHOAdc B4AHp Aes B9AAAAAAAAAIBAAAAAAAAAEk A
AAAAAAAAAAAAAAAAAAH9
Shining Light on Solar
대부분의 우리는 이 알 수 없는 글자의 벽을 보고선 조용히 맥북을 닫았을 것입니다. 하지만 몇 분은 아셨을 사실인데 이 텍스트는 Base64-encoded로 암호화된 것과 아주 비슷하게 생겼습니다.
우리의 가설을 실행에 옮길 시간입니다.
if name == "solar" {
let data = Data(base64Encoded: value)!
print(String(data: data, encoding: .ascii))
}
bplist00Ò\u{01}\u{02}\u{03}...
bplist
가 뭘까요?
놀랍게도 이건 바이너리 프로퍼티 리스트의 파일 서명입니다.
이번엔 Property
를 사용해보겠습니다…
if name == "solar" {
let data = Data(base64Encoded: value)!
let property List = try Property List Serialization
.property List(from: data,
options: [],
format: nil)
print(property List)
}
(
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
는 고도(altitude)와 방위각(azimuth)를 생각하시면 쉽습니다.
태양의 위치 계산하기
이 글을 쓰고 있는 시점엔 북반구에 있는 우리의 계절은 가을이며 날은 추워졌고 해가 짧아졌습니다. 남반구는 반대로 날이 따뜻해지고 해가 길어졌겠죠. 계절의 변화는 태양의 길이는 곧 우리가 어디에 있는지에 따라 다르다는 것을 생각하게 해줍니다.
좋은 소식은 천문학이 정확히 왜 그런지 알려줄 수 있다는 것입니다. 나쁜 소식은 그걸 설명하려면 아주 복잡한 계산이 필요하다는 것입니다.
솔직히 말하자면 우리는 우리 자신을 이해시킬 필요가 없습니다. 우리는 그저 인터넷에서 찾은 내용을 포팅하면 됩니다. 몇번의 시도와 에러 끝에 저희는 실제로 작동하는 코드에 어느정도 도달한 것 같습니다. (PR은 환영입니다!)
import Foundation
import Core Location
// Apple Park, Cupertino, CA
let location = CLLocation(latitude: 37.3327, longitude: -122.0053)
let time = Date()
let position = solar Position(for: location, at: time)
let formatted Date = Date Formatter.localized String(from: time,
date Style: .medium,
time Style: .short)
print("Solar Position on \(formatted Date)")
print("\(position.azimuth)° Az / \(position.elevation)° El")
2018년 10월 1일 12시의 태양 위치 180.73470025840783° Az / 49.27482549913847° El
2018년 10월 1일 정오에 Apple Park의 태양빛은 남쪽에서 오고 수평선과 머리 바로 위의 사이에서 비치고 있었습니다.
하루 종일 태양의 위치를 추적한다면 우리는 Apple Watch의 “Solar” 페이스를 닮은 사인 그래프 모양을 얻게 될 것입니다.
XMP에 대한 이해 확장하기
좋습니다. 천문학은 충분한 것 같습니다. 이제 조금 지루한 내용으로 가보겠습니다. XML 메타데이터 표준입니다.
has
메타데이터 키를 기억하시나요? 그겁니다.
XMP 또는 Extensible Metadata Platform은 메타데이터로 파일을 태깅하는 표준 형식입니다. XMP는 어떻게 생겼을까요? 마음 단단히 먹으세요!
let xmp Data = CGImage Metadata Create XMPData(metadata, nil)
let xmp = String(data: xmp Data 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>
웩
우리는 apple_desktop
라는 이름이 우리가 만든 Dynamic Desktop 이미지에도 잘 작동하는지 확인할 필요가 있습니다.
말하자면 이제 시작이라는거죠.
자기만의 Dynamic Desktop 만들기
Dynamic Desktop을 표현하는 데이터 모델을 만들어보겠습니다.
struct Dynamic Desktop {
let images: [Image]
struct Image {
let cg Image: CGImage
let metadata: Metadata
struct Metadata: Codable {
let index: Int
let altitude: Double
let azimuth: Double
private enum Coding Keys: String, Coding Key {
case index = "i"
case altitude = "a"
case azimuth = "z"
}
}
}
}
각 Dynamic Desktop은 정렬된 이미지 시퀀스로 이루어져 있습니다. 이 이미지는 CGImage
객체가 저장된 이미지 데이터와 이전에 다뤘던 메타데이터로 이루어져 있습니다.
우리는 컴파일러가 자동으로 일을 마치게 하기 위해 Metadata
선언시에 Codable
을 사용합니다.
Base64로 인코딩된 바이너리 프로퍼티 리스트를 생성할 때 이를 활용할 것입니다.
이미지 경로에 작성하기
먼저 특정 URL로 CGImage
를 생성합니다.
파일 타입은 heic
이고 원본 숫자는 포함돼있던 것과 동일합니다.
guard let image Destination = CGImage Destination Create With URL(
output URL as CFURL,
AVFile Type.heic as CFString,
dynamic Desktop.images.count,
nil
)
else {
fatal Error("Error creating image destination")
}
다음으로 dynamic desktop 객체의 각 이미지를 순환합니다.
enumerated()
메소드를 사용하면 우리는 반복문 속에서도 현재 index
를 알 수 있으니 첫 번째 이미지의 메타데이터 값을 설정해보겠습니다.
for (index, image) in dynamic Desktop.images.enumerated() {
if index == 0 {
let image Metadata = CGImage Metadata Create Mutable()
guard let tag = CGImage Metadata Tag Create(
"http://ns.apple.com/namespace/1.0/" as CFString,
"apple_desktop" as CFString,
"solar" as CFString,
.string,
try! dynamic Desktop.base64Encoded Metadata() as CFString
),
CGImage Metadata Set Tag With Path(
image Metadata, nil, "xmp:solar" as CFString, tag
)
else {
fatal Error("Error creating image metadata")
}
CGImage Destination Add Image And Metadata(image Destination,
image.cg Image,
image Metadata,
nil)
} else {
CGImage Destination Add Image(image Destination,
image.cg Image,
nil)
}
}
Core Graphics API의 정제되지 않은 특징 외에는 위의 코드는 꽤 쉽습니다.
추가적인 설명이 필요한 부분은 CGImage
를 호출하는 부분뿐입니다.
이미지와 메타데이터가 이루어진 방식과 코드에서 표현되는 방식의 괴리때문에 우리는 Dynamic
을 위한 Encodable
을 직접 만들어야 했습니다.
extension Dynamic Desktop: Encodable {
private enum Coding Keys: String, Coding Key {
case ap, si
}
private enum Nested Coding Keys: String, Coding Key {
case d, l
}
func encode(to encoder: Encoder) throws {
var keyed Container =
encoder.container(keyed By: Coding Keys.self)
var nested Keyed Container =
keyed Container.nested Container(keyed By: Nested Coding Keys.self,
for Key: .ap)
// 도와주세요: `l`과 `d`가 정확히 나타내는 값을 모르겠어요
try nested Keyed Container.encode(0, for Key: .l)
try nested Keyed Container.encode(self.images.count, for Key: .d)
var unkeyed Container =
keyed Container.nested Unkeyed Container(for Key: .si)
for image in self.images {
try unkeyed Container.encode(image.metadata)
}
}
}
위의 Encodable이 완성되면 앞에서 언급했던 base64Encoded
메소드를 다음과 같이 구현할 수 있을 것입니다.
extension Dynamic Desktop {
func base64Encoded Metadata() throws -> String {
let encoder = Property List Encoder()
encoder.output Format = .binary
let binary Property List Data = try encoder.encode(self)
return binary Property List Data.base64Encoded String()
}
}
for-in 반복문이 끝나서 모든 이미지와 메타데이터가 작성되고나면 이제 CGImage
를 호출해서 이미지를 마무리하고 디스크에 저장하면 됩니다.
guard CGImage Destination Finalize(image Destination) else {
fatal Error("Error finalizing image")
}
모든 것이 예상했던 대로 작동한다면 이제 새로운 Dynamic Desktop의 주인이 된 것을 자랑스럽게 여기셔도 됩니다. 끝내주네요!
Mojave의 새로운 기능인 Dynamic Desktop을 사랑하며 윈도우 95에서 대유행을 했던 배경화면과 비슷한 것이 다시 나와서 정말 즐겁습니다.
여러분도 그렇게 생각하신다면 다음과 같은 아이디어를 시도해보세요.
사진에서 자동으로 Dynamic Desktop 생성하기
천체의 움직임처럼 엄청난 것이 시간과 장소라는 두 가지 입력의 방정식으로 축소될 수 있다는 것은 정말 충격적입니다.
이전의 예시처럼 이 정보는 하드 코딩돼있었지만 이미지에서 정보를 추출하는 것은 자동으로 할 수 있었습니다.
기본적으로 대부분의 핸드폰의 카메라는 사진을 찍을때마다 Exif 메타데이터 값도 같이 저장합니다. 이 메타데이터는 사진이 찍힌 시간과 기기의 위치 정보(GPS)를 포함할 수 있습니다.
이미지 메타데이터에서 시간과 위치 정보를 직접 읽으면 우리는 자동으로 태양의 위치를 계산할 수 있으며 사진 시리즈를 통해서 Dynamic Desktop를 만드는 과정을 간편하게 만들 수 있을 것입니다.
iPhone의 타임 랩스 이용하기
iPhone XS를 좋은 곳에 사용하고 싶으신가요? (더 정확히 말하자면, “오래된 iPhone을 팔기 전에 생산적인 활동에 사용해보고 싶으신가요?”)
창문에 아이폰을 놓고, 충전기에 끼운 다음, 카메라를 타임 랩스 모드로 설정한 후에, “녹화” 버튼을 누르세요. 결과로 나온 비디오에서 키 프레임을 추출해내면 여러분만의 맞춤 제작 Dynamic Desktop을 만들 수 있을 것입니다.
Skyflow나 비슷한 앱을 사용하면 더 쉽게 지정된 간격으로 사진을 찍을 수 있습니다.
GIS 데이터로 풍경 생성하기
아이폰과 하루라도 떨어져 있을 수 없거나 (슬프네요) 기억할만한 녹화거리가 없다면 (이것도 슬프네요), 현실을 창조해내면 됩니다. (가장 슬픈 얘기네요)
Terragen같은 앱을 사용하면 현실같은 3D 풍경 사진을 만들 수 있습니다. 게다가 땅과 태양, 하늘까지 다 조정할 수 있습니다.
미국의 Geological Survey의 National Map 웹사이트에서 다운로드 받아서 3D 렌더링 프로젝트의 템플릿에 사용하면 일이 더 쉬워질 것입니다.
이미 만들어진 Dynamic Desktop 다운로드받기
해야 할 일이 많고 이쁜 사진을 만드는 데에 쓸 시간이 없다면 항상 방법은 있습니다. 바로 다른 누군가에게 돈을 주고 사는 것이죠.
저희는 개인적으로 24 Hour Wallpaper의 팬입니다.
또 다른 추천할 것이 있다면 트위터에서 태그해주세요!