Escritorio Dinámico de macOS
Para nosotros, los desarrolladores que tendemos a preferir temas claros sobre fondos oscuros en nuestros editores de texto, pero también apreciamos la consistencia a través de todo el sistema operativo, el nuevo Modo Oscuro representa una de las características más agradables agregadas a macOS en los últimos años.
Night Shift se llevó las palmas hace un par de años por aliviar la fatiga visual en esas sesiones de programación que terminan ya sea muy tarde en la noche o muy temprano en la mañana.
Al combinar ambas funcionalidades, tenemos como resultado la nueva característica introducida en Mojave, Escritorio Dinámico. La opción se encuentra disponible al acceder a “Preferencias del Sistema > Escritorio y Salvapantallas”, la imagen cambiará durante el día, basado en tu ubicación.
El resultado es sutil pero significativo. El ver cómo transiciona el fondo de pantalla durante el día, sincronizado con el mundo real, hace que nuestro escritorio cobre vida o por lo menos, nos da un hermoso efecto visual durante la transición al encender y apagar Modo Oscuro.
Pero, la pregunta es ¿cómo funciona?
Ése es el tema de esta semana en NSHipster.
La respuesta involucra adentrarnos en las profundidades de los distintos formatos de imágenes, un poco de ingeniería inversa e incluso una pizca de trigonometría esférica.
Para entender cómo funciona Escritorio Dinámico el primer paso es obtener una imagen dinámica.
Si estás usando macOS Mojave, abre el Finder y selecciona “Ir > Ir a la carpeta…” (⇧⌘G), e ingresa la siguiente ruta: “/Library/Desktop Pictures/”.
En esta carpeta debería existir un archivo llamado “Mojave.heic”, dale doble clic para abrirlo con Vista Previa.
Usando Vista Previa, podemos ver que la barra lateral muestra miniaturas numeradas del 1 al 16, cada una de ellas contiene una vista distinta de la escena del desierto.
Al seleccionar “Herramientas > Mostrar Inspector (⌘I), podemos ver información general acerca de la imagen:
Desafortunadamente, la información que nos da Vista Previa, por lo menos al momento de escritura del artículo, es insuficiente. Inclusive, si hacemos clic al panel contiguo llamado “Inspector de información adicional”, los datos obtenidos resultan poco satisfactorios.
Color Model | RGB |
Depth: | 8 |
Pixel Height | 2,880 |
Pixel Width | 5,120 |
Profile Name | Display P3 |
Para saber más sobre esta imagen, tocará ensuciarse las manos al interactuar con algunas APIs de bajo nivel.
Investigando con CoreGraphics
Empecemos nuestra labor con un nuevo Playground en Xcode. Para no complicar de más las cosas, usemos la dirección a “Mojave.heic” en nuestra computadora.
import Foundation
import Core Graphics
// mac OS 10.14 Mojave Required
let url = URL(file URLWith Path: "/Library/Desktop Pictures/Mojave.heic")
Vamos a crear una instancia de CGImage
, copiar sus metadatos y luego iterar sobre cada una de sus etiquetas:
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)
}
Al ejecutar este código obtenemos dos resultados, el primero has
con un valor de "True"
y el segundo solar
con un valor no tan obvio:
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
Desentrañando el secreto solar
Después de ver el valor de esa variable, la mayoría de nosotros apagaría prontamente nuestra computadora y seguiríamos con nuestro día. Sin embargo, como algunos habrán notado, la cadena de texto se parece mucho a una cadena en base64.
Probemos nuestra hipótesis:
if name == "solar" {
let data = Data(base64Encoded: value)!
print(String(data: data, encoding: .ascii))
}
bplist00Ò\u{01}\u{02}\u{03}...
¿Qué se supone que significa bplist
? ¿Y lo que sigue? Pensándolo bien, eso parece ser la firma de un archivo binario de propiedades.
Pidamos ayuda a 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";
}
)
)
¡Finalmente!
Al parecer tenemos dos llaves en el nivel más alto:
- La llave
ap
que corresponde a un diccionario de enteros para las llavesd
yl
. - La llave
si
que corresponde a un arreglo de diccionarios con enteros y números punto flotantes. Dentro de las llaves anidadasi
es la más fácil de entender, incrementa de 0 hasta 15 y representa el índice de la imagen dentro de la secuencia. Es difícil saber a ciencia cierta sin información adicional pero representan la altitud (a
) y el azimuth (z
) del sol en la imagen.
Calculando la posición del sol
Al momento de escribir este artículo, aquellos de nosotros en el hemisferio norte estamos entrando al otoño, con sus días cortos y fríos. Mientras los que se encuentran en el hemisferio sur se preparan para días más largos y calientes. El cambio de estaciones nos recuerda que la duración de un día solar depende de varios factores, tu ubicación y la ubicación del planeta en su órbita alrededor del astro rey.
Tenemos buenas y malas noticias, las buenas son que los astrónomos nos pueden decir, sin lugar a equivocaciones, dónde se encuentra el sol en cualquier tiempo o lugar. Las malas noticias son que los cálculos son algo complejos.
Sin ánimo de engañar al lector, nosotros mismos no lo entendemos y lo que hicimos fue migrar código que encontramos en línea. Después de algunas mejoras, a punta de prueba y error, logramos llegar a una solución que parece funcionar (aceptamos tus Pull Requests):
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")
Solar Position on Oct 1, 2018 at 12:00 180.73470025840783° Az / 49.27482549913847° El
El primero de octubre del 2018, a mediodía, el sol brilla sobre el Apple Park desde el sur, más o menos a la mitad entre el horizonte y encima de nosotros.
Si lleváramos un registro de la posición del sol durante todo un día, obtendríamos una onda sinusoidal, similar a la que se observa en la carátula llamada “Solar” del Apple Watch.
Cara a cara con XMP
Dejando de lado la clase de astronomía por un rato, enfoquémonos en algo menos emocionante, el uso de XML como la base de estándares para metadatos.
Hace algunos párrafos hablámos de la llave has
, sip ésa llave. La Plataforma Extensible de Metadatos, conocida por sus siglas en inglés como XMP, es un tipo de lenguaje de marcado para representar metadatos en archivos. Salta la pregunta obvia, ¿y cómo se ve? (No apto para aquellos con problemas cardíacos):
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>
Un asco.
No todo es negativo, podemos rescatar de nuestra misión de excavación la declaración del namespace
“apple_desktop”, punto importante que nos indica que ocupamos el nombre para las etiquetas de nuestros XMLs.
Manos a la obra.
Construyamos nuestro Escritorio Dinámico
Iniciemos con un modelo que represente un Escritorio Dinámico:
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"
}
}
}
}
Cada Escritorio Dinámico está compuesto de una secuencia de imágenes, cada una de ellas tiene datos almacenados en un objeto de tipo CGImage
y los metadatos cubiertos anteriormente. Implementamos Codable
en la declaración de nuestro model Metadata
para indicar al compilador que genere (o sintetice) automáticamente la conformación al protocolo. Aprovecharemos lo anterior cuando tengamos que generar la lista binaria de propiedades en Base64.
Creando un destino para la imagen
Vamos a crear una instancia de CGImage
especificando la ruta final como destino, usando heic
como el formato y el total de imágenes de entrada será igual a la cantidad de imágenes que vayamos a incluir.
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")
}
Seguidamente, usamos el método enumerated()
para iterar sobre cada una de las imágenes de nuestro objeto dynamic
. Vamos a ocupar el índice para cada iteración, para poder establecer los medatatos de la primera imagen.
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)
}
}
Ignorando la crudeza de las APIs de Core Graphics, el código es fácil de seguir, la única parte que ocupa ser explicada es la llamada a CGImage
.
Debido a que existe una discrepancia entre la estructura de los metadatos de las imágenes y los del contenedor y su debida representación, debemos implementar Encodable
para nuestro modelo Dynamic
:
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)
// FIXME: Not sure what `l` and `d` keys indicate
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)
}
}
}
Usando lo anterior como base, podemos finalmente implementar el método base64Encoded
de la siguiente manera:
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()
}
}
Después de escribir los metadatos en todas las imágenes, debemos invocar a CGImage
para dar fin a nuestra imagen y guardar el resultado en el disco:
guard CGImage Destination Finalize(image Destination) else {
fatal Error("Error finalizing image")
}
Si todo funcionó, seremos los orgullosos dueños de un nuevo un Escritorio Dinámico, ¡felicidades!
La nueva característica introducida en Mojave, Escritorio Dinámico, nos ha vuelto locos, ojalá y la veamos popularizarse al mismo nivel que lograron los fondos de pantalla de Windows 95.
La siguiente sección está compuesta de ejercicios para el lector. Sin más demora:
Generar un Escritorio Dinámico usando fotos
Es increíble que algo tan magno como el movimiento de cuerpos celestiales se pueda reducir a un sistema de ecuaciones con dos entradas: fecha y lugar.
En el ejemplo anterior, ésta información está hardcoded, sin embargo la misma información puede ser extraída automáticamente de las imágenes.
De manera predeterminada, las cámaras en los teléfonos modernos almacenan metadatos Exif cada vez que se captura una foto. Dentro de la información almacenada se encuentra la fecha y las coordenadas GPS del dispositivo en el momento de la captura.
Usando la fecha y la ubicación de los metadatos de la imagen podemos determinar la posición solar y así simplificar el proceso de generar un Escritorio Dinámico desde un conjunto de fotos.
Usar un iPhone para capturar un time-lapse
Pongamos ese nuevo iPhone XS a trabajar… o mejor dicho, pongamos el iPhone viejo a hacer algo útil mientras ignoramos que debíamos haberlo vendido.
Fijemos el teléfono contra una ventana, conéctalo a un cargador y establece el modo de la cámara a time-lapse y pulsa el botón “Grabar”. Usando algunos cuadros clave de tu video podrás crear tu Escritorio Dinámico a medida.
Otra opción es usar alguna aplicación como Skyflow que permite tomar fotos usando intervalos definidos.
Generar un paisaje usando datos GIS
En caso de no poder despegarte del teléfono un día entero (qué triste) o de no tener nada interesante por capturar (también muy triste) siempre existe la opción de crear tu propia realidad (suena más triste de lo que realmente es).
Puedes usar una aplicación como Terragen para dibujar paisajes realistas en 3D teniendo control preciso sobre la tierra, el sol y el cielo.
Incluso te puedes facilitar la tarea un poco más al descargar mapas de elevación producto de los estudios geológicos tomados por el United States Geological Survey y disponibles en el sitio web de The National Map. Usando estos mapas como plantillas podrás acelerar la creación de tu paisaje 3D.
Descargar Escritorios Dinámicos listos
Si, por el contrario, estás ocupado y no tienes tiempo para hacer fotos bonitas, siempre se puede pagar a alguien que lo haga por ti.
Personalmente nos gusta la aplicación 24 Hour Wallpaper, pero estamos abiertos a sugerencias, contáctanos en Twitter con la tuya.