Initial Application
This commit is contained in:
133
docs/BUILD.md
Normal file
133
docs/BUILD.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Building & Compiling
|
||||
|
||||
This document covers everything involved in turning the source tree into a
|
||||
running application: the Java/Maven build, and the native Swift helper that
|
||||
Java compiles for itself the first time it needs it.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Tool | Why | Check |
|
||||
|---|---|---|
|
||||
| macOS | Dynamic Desktop is a macOS-only feature | `sw_vers` |
|
||||
| JDK 17 or newer | Compiles and runs the Swing application | `java -version` |
|
||||
| Apache Maven 3.6+ | Drives the Java build | `mvn -version` |
|
||||
| Xcode Command Line Tools | Provides `swiftc`, used to compile the native HEIC helper | `xcrun --find swiftc` |
|
||||
|
||||
If `xcrun --find swiftc` fails, install the Command Line Tools with:
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
You do **not** need the full Xcode IDE — the Command Line Tools package is
|
||||
enough, and is what most Mac developer machines already have.
|
||||
|
||||
## 2. Building the Java application
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
mvn package
|
||||
```
|
||||
|
||||
This does two things:
|
||||
|
||||
1. Compiles all classes under `src/main/java` with the `maven-compiler-plugin`.
|
||||
2. Uses the `maven-assembly-plugin` to produce a single **runnable, self-contained
|
||||
jar** at `target/dynamic-wallpaper-creator.jar` (there are no third-party
|
||||
runtime dependencies to bundle — the app only uses the JDK's own Swing,
|
||||
AWT and I/O APIs — so this step mainly just sets the executable
|
||||
`Main-Class` manifest entry).
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
java -jar target/dynamic-wallpaper-creator.jar
|
||||
```
|
||||
|
||||
Other useful Maven targets:
|
||||
|
||||
```bash
|
||||
mvn compile # compile only, no jar
|
||||
mvn clean # remove target/
|
||||
mvn -q package # quieter output
|
||||
```
|
||||
|
||||
There are no automated tests in this project (see "Testing strategy" below
|
||||
for why, and how the app was actually validated).
|
||||
|
||||
## 3. Building the native helper (`heic-builder`)
|
||||
|
||||
Java cannot write HEIC files with multiple embedded images and custom XMP
|
||||
metadata on its own — those capabilities only exist in Apple's ImageIO and
|
||||
CoreGraphics frameworks, which are Objective-C/Swift APIs. To bridge that
|
||||
gap, the app ships the source for a small command-line tool,
|
||||
[`src/main/resources/native/HeicBuilder.swift`](../src/main/resources/native/HeicBuilder.swift),
|
||||
as a resource inside the jar.
|
||||
|
||||
You never compile this file yourself — the Java app does it automatically:
|
||||
|
||||
1. On startup (specifically, the first time you click **Generate
|
||||
Wallpaper**), `com.dynamicwallpaper.core.NativeHelper` extracts the
|
||||
bundled `.swift` source to
|
||||
`~/Library/Application Support/DynamicWallpaperCreator/HeicBuilder.swift`.
|
||||
2. It computes a SHA-256 hash of that source and compares it against the hash
|
||||
recorded the last time it compiled. If they differ (first run, or you
|
||||
built a newer version of the app with an updated helper), it invokes:
|
||||
```bash
|
||||
xcrun swiftc -O HeicBuilder.swift -o heic-builder
|
||||
```
|
||||
in that same directory, and records the new hash.
|
||||
3. Every subsequent run just reuses the cached `heic-builder` binary — no
|
||||
recompilation, effectively instant startup.
|
||||
|
||||
If you want to trigger this manually (e.g. to debug it in isolation), you
|
||||
can compile and run it directly:
|
||||
|
||||
```bash
|
||||
xcrun swiftc -O src/main/resources/native/HeicBuilder.swift -o /tmp/heic-builder
|
||||
/tmp/heic-builder build path/to/job.json
|
||||
/tmp/heic-builder inspect path/to/some.heic # dumps embedded apple_desktop metadata, for debugging
|
||||
```
|
||||
|
||||
See [FILE_FORMAT.md](FILE_FORMAT.md) for the `job.json` schema this tool
|
||||
expects.
|
||||
|
||||
## 4. Testing strategy
|
||||
|
||||
There is no JUnit suite bundled with this project. The reasons and what was
|
||||
actually done instead:
|
||||
|
||||
- The two things worth testing — "does the GUI look/behave right" and "is the
|
||||
generated `.heic` file byte-for-byte structured the way macOS expects" —
|
||||
are not meaningfully covered by unit tests. The second one *can* be
|
||||
verified programmatically, and was: during development, the native
|
||||
helper's `inspect` command was used to decode the metadata out of a
|
||||
freshly generated file and diff it against the metadata pulled from a real
|
||||
Apple-shipped wallpaper (`/System/Library/Desktop Pictures/Sonoma.heic`).
|
||||
They match exactly (see [FILE_FORMAT.md](FILE_FORMAT.md)).
|
||||
- If you're extending this project and want a smoke test, the fastest way is
|
||||
the `inspect` command above: generate a file, inspect it, and confirm the
|
||||
`apple_desktop:apr` or `apple_desktop:solar` tag decodes to the plist
|
||||
structure you expect.
|
||||
- For a full manual test, generate a wallpaper, click **Set as Desktop
|
||||
Picture**, then toggle System Settings → Appearance between Light and Dark
|
||||
(for appearance-mode wallpapers) and confirm the desktop image changes.
|
||||
|
||||
## 5. Packaging for distribution (optional)
|
||||
|
||||
The runnable jar from `mvn package` is enough to hand to another Mac user —
|
||||
they just need Java 17+ and Xcode Command Line Tools installed, and can run
|
||||
`java -jar dynamic-wallpaper-creator.jar`. If you want a double-clickable
|
||||
`.app` bundle instead, wrap the jar with `jpackage` (bundled with the JDK):
|
||||
|
||||
```bash
|
||||
jpackage \
|
||||
--input target \
|
||||
--name "Dynamic Wallpaper Creator" \
|
||||
--main-jar dynamic-wallpaper-creator.jar \
|
||||
--main-class com.dynamicwallpaper.Main \
|
||||
--type app-image
|
||||
```
|
||||
|
||||
This is optional and not required for local development or use.
|
||||
186
docs/FILE_FORMAT.md
Normal file
186
docs/FILE_FORMAT.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# The macOS Dynamic Desktop `.heic` File Format
|
||||
|
||||
This document explains exactly what bytes this app writes and why, for
|
||||
anyone extending the project or just curious how Dynamic Desktop wallpapers
|
||||
work under the hood. Apple has never published a specification for this
|
||||
format — everything below was independently reverse-engineered by the
|
||||
macOS developer community over the years, and was **re-verified during the
|
||||
development of this app** by decoding a real, Apple-shipped dynamic
|
||||
wallpaper file directly (see "Verification" below).
|
||||
|
||||
## 1. Container: HEIF/HEIC with multiple top-level images
|
||||
|
||||
A `.heic` file is a HEIF container (the same family of formats used for
|
||||
iPhone photos). Normally it holds exactly one image. A Dynamic Desktop
|
||||
wallpaper is the same container format, just holding **several independent,
|
||||
full-resolution images** as separate top-level items instead of one — think
|
||||
of it like a zip file with N pictures inside, rather than one image with N
|
||||
animation frames.
|
||||
|
||||
macOS's own Photos/Preview/QuickLook stack can create files like this
|
||||
through `CGImageDestination`, by calling `CGImageDestinationAddImage`
|
||||
repeatedly on the same destination before finalizing it. That's exactly
|
||||
what [`HeicBuilder.swift`](../src/main/resources/native/HeicBuilder.swift)
|
||||
does:
|
||||
|
||||
```swift
|
||||
let dest = CGImageDestinationCreateWithURL(url, "public.heic", images.count, nil)
|
||||
for (index, image) in images.enumerated() {
|
||||
CGImageDestinationAddImage(dest, image, properties) // (image 0 also carries metadata, see below)
|
||||
}
|
||||
CGImageDestinationFinalize(dest)
|
||||
```
|
||||
|
||||
This is a capability of Apple's ImageIO framework specifically — there is no
|
||||
equivalent in Java's `ImageIO`, and general-purpose command-line HEIC
|
||||
encoders like `heif-enc` don't expose it either (they support single images,
|
||||
"bursts"/animation sequences via `-S`, or resolution pyramids, but not
|
||||
independent top-level image collections). That's the whole reason this
|
||||
project ships a small Swift helper rather than doing everything in pure
|
||||
Java — see [BUILD.md](BUILD.md) for how that helper is compiled.
|
||||
|
||||
## 2. Metadata: a binary property list, base64-encoded, in a custom XMP tag
|
||||
|
||||
Attached to **image index 0 only**, there's an XMP metadata tag under a
|
||||
private Apple namespace:
|
||||
|
||||
- Namespace URI: `http://ns.apple.com/namespace/1.0/`
|
||||
- Prefix: `apple_desktop`
|
||||
- Tag name: `apr` (appearance mode) or `solar` (sun-position mode)
|
||||
- Value: a `bplist00` (Apple binary property list), serialized to bytes, then
|
||||
base64-encoded into a plain string, which is what actually gets stored as
|
||||
the XMP tag's value.
|
||||
|
||||
In Swift this looks like:
|
||||
|
||||
```swift
|
||||
let plistData = try PropertyListSerialization.data(fromPropertyList: dict, format: .binary, options: 0)
|
||||
let base64 = plistData.base64EncodedString()
|
||||
let tag = CGImageMetadataTagCreate(namespace, "apple_desktop", "apr", .string, base64 as CFTypeRef)
|
||||
```
|
||||
|
||||
### 2a. Appearance mode (`apple_desktop:apr`)
|
||||
|
||||
Exactly two images: index 0 is shown in Light Mode, index 1 in Dark Mode. The
|
||||
plist is a flat dictionary:
|
||||
|
||||
```
|
||||
{
|
||||
l = 0; // index of the "light" image
|
||||
d = 1; // index of the "dark" image
|
||||
}
|
||||
```
|
||||
|
||||
### 2b. Solar mode (`apple_desktop:solar`)
|
||||
|
||||
Any number of images (Apple's own wallpapers typically use 16), each tagged
|
||||
with the sun's position when it should be shown:
|
||||
|
||||
```
|
||||
{
|
||||
si = (
|
||||
{ i = 0; a = -10.0; z = 60.0; }, // i = image index, a = altitude (degrees), z = azimuth (degrees)
|
||||
{ i = 1; a = 15.0; z = 90.0; },
|
||||
{ i = 2; a = 60.0; z = 180.0; },
|
||||
...
|
||||
);
|
||||
ap = { l = 2; d = 0; }; // which frame index best represents "light" / "dark" for accessibility fallback
|
||||
}
|
||||
```
|
||||
|
||||
- **Altitude (`a`)** — the sun's height above the horizon in degrees. `0` is
|
||||
the horizon, `90` is directly overhead, negative values are below the
|
||||
horizon (night).
|
||||
- **Azimuth (`z`)** — the sun's compass direction in degrees, `0`–`360`.
|
||||
- **`ap`** — a secondary hint (same `l`/`d` shape as appearance mode) telling
|
||||
macOS which single frame to fall back to when it just needs "a light one"
|
||||
or "a dark one" rather than the full solar animation (e.g. for
|
||||
accessibility or low-power contexts). This app computes it automatically
|
||||
from whichever image has the highest/lowest altitude, unless you
|
||||
explicitly check "Light Ref." / "Dark Ref." on a specific row in the
|
||||
Solar tab.
|
||||
|
||||
This app does not currently implement the third known mode, time-of-day
|
||||
scheduling (`apple_desktop:h24`, keyed by `ti`/`t`/`i` instead of `si`/`a`/`z`),
|
||||
since it wasn't part of the request this project was built for. It would
|
||||
slot into `HeicBuilder.swift` the same way `solar` does, if needed later.
|
||||
|
||||
## 3. Verification against a real Apple wallpaper
|
||||
|
||||
Rather than trusting secondhand write-ups alone, the exact schema above was
|
||||
confirmed directly against a dynamic wallpaper Apple ships with macOS,
|
||||
`/System/Library/Desktop Pictures/Sonoma.heic`, using the `inspect` command
|
||||
built into the native helper:
|
||||
|
||||
```
|
||||
$ heic-builder inspect "/System/Library/Desktop Pictures/Sonoma.heic"
|
||||
images: 2
|
||||
image 0 apple_desktop:apr = {
|
||||
d = 1;
|
||||
l = 0;
|
||||
}
|
||||
```
|
||||
|
||||
That is a live decode of Apple's own file — namespace, prefix, tag name
|
||||
(`apr`), and the `l`/`d` key structure all matched what this project already
|
||||
implemented. The `solar` structure (`si`/`ap`/`i`/`a`/`z`) is corroborated by
|
||||
multiple independent community write-ups (NSHipster's "macOS Dynamic
|
||||
Desktop" article and a widely cited 2018 reverse-engineering gist), and this
|
||||
project's own `heic-builder inspect` command was used to confirm that files
|
||||
*this app generates* round-trip through Apple's ImageIO the same way: build
|
||||
a file, inspect it back, and the decoded plist matches what was requested
|
||||
byte-for-byte.
|
||||
|
||||
If you ever need to re-verify or debug this yourself, `heic-builder inspect`
|
||||
works on any `.heic` file, including ones this app produced or ones from
|
||||
`/System/Library/Desktop Pictures/`.
|
||||
|
||||
## 4. The `job.json` hand-off format
|
||||
|
||||
This is purely an internal implementation detail — the Java GUI never writes
|
||||
a `.heic` file itself; it writes a small JSON description of what to build to
|
||||
a temp file, and runs `heic-builder build <that file>`. Documented here in
|
||||
case you want to drive the helper directly (e.g. from a script) without going
|
||||
through the GUI at all.
|
||||
|
||||
**Appearance mode:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "appearance",
|
||||
"output": "/absolute/path/out.heic",
|
||||
"quality": 0.9,
|
||||
"light": "/absolute/path/light.png",
|
||||
"dark": "/absolute/path/dark.png"
|
||||
}
|
||||
```
|
||||
|
||||
**Solar mode:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "solar",
|
||||
"output": "/absolute/path/out.heic",
|
||||
"quality": 0.9,
|
||||
"images": [
|
||||
{ "path": "/absolute/path/01.png", "altitude": -10.0, "azimuth": 60.0,
|
||||
"lightReference": false, "darkReference": true },
|
||||
{ "path": "/absolute/path/02.png", "altitude": 60.0, "azimuth": 180.0,
|
||||
"lightReference": true, "darkReference": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `quality` is the HEIC lossy compression quality, `0.0`–`1.0` (higher = better
|
||||
quality, larger file). Optional, defaults to `0.9`.
|
||||
- `lightReference` / `darkReference` are optional booleans; at most one image
|
||||
should have each set to `true`. If neither is set anywhere, the helper
|
||||
picks the highest-altitude image as the light reference and the
|
||||
lowest-altitude image as the dark reference automatically.
|
||||
- Input images should all share the same pixel dimensions. The Java app
|
||||
warns (non-fatally) if they don't; the helper itself does not resize them.
|
||||
- `heic-builder build` prints one line of progress per image to stdout, then
|
||||
either `OK <output path>` on success (exit code 0) or `ERROR: <message>`
|
||||
to stderr (non-zero exit code) on failure. The Java app streams this
|
||||
output straight into the log panel at the bottom of the window.
|
||||
98
docs/USAGE.md
Normal file
98
docs/USAGE.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Using Dynamic Wallpaper Creator
|
||||
|
||||
This walks through the app window shown by `java -jar
|
||||
target/dynamic-wallpaper-creator.jar` (see [BUILD.md](BUILD.md) if you
|
||||
haven't built it yet).
|
||||
|
||||
## 1. Pick a wallpaper type
|
||||
|
||||
The dropdown at the top switches between the two supported wallpaper types:
|
||||
|
||||
- **Light / Dark Appearance** — the simple case: one image for Light Mode,
|
||||
one for Dark Mode. Good for a first try, or if you just want two specific
|
||||
images to swap automatically.
|
||||
- **Sun Position (Solar)** — the full "Dynamic Desktop" experience: any
|
||||
number of images, each tied to a point in the sun's daily arc, cross-fading
|
||||
as the actual time of day changes.
|
||||
|
||||
## 2. Light / Dark Appearance tab
|
||||
|
||||
Two side-by-side cards, one per appearance. For each:
|
||||
|
||||
1. Click **Choose Image…**.
|
||||
2. Pick a PNG, JPEG, TIFF, BMP, GIF, or HEIC file.
|
||||
|
||||
A thumbnail preview appears once selected. Both a light and a dark image are
|
||||
required before you can generate.
|
||||
|
||||
**Tip:** the two images should be the same pixel dimensions — same scene,
|
||||
different lighting, works best. The app warns you (but doesn't block you) if
|
||||
they don't match.
|
||||
|
||||
## 3. Sun Position (Solar) tab
|
||||
|
||||
1. Click **Add Images…** and select one or more image files (multi-select is
|
||||
supported in the file picker). Each becomes a row in the table.
|
||||
2. For each row, set:
|
||||
- **Altitude (°)** — how high the sun is: `90` = directly overhead, `0` =
|
||||
right on the horizon, negative values = below the horizon (night/dusk).
|
||||
- **Azimuth (°)** — the sun's compass direction, `0`–`360`.
|
||||
- **Light Ref. / Dark Ref.** checkboxes (optional) — mark the single
|
||||
frame that best represents "daytime" and the one that best represents
|
||||
"nighttime". If you leave these unchecked, the app picks them for you
|
||||
automatically (highest and lowest altitude).
|
||||
3. Don't want to hand-enter every value? Select at least two images and
|
||||
click **Auto-Distribute Sun Positions** — it fills in a smooth
|
||||
sunrise → noon → sunset → night arc across however many images you've
|
||||
added, evenly spaced. You can still tweak any individual cell afterward
|
||||
by double-clicking it.
|
||||
4. Use **Move Up** / **Move Down** to reorder rows, or **Remove Selected** /
|
||||
**Clear All** to edit the set. Order doesn't affect how macOS displays the
|
||||
wallpaper (that's entirely driven by the altitude/azimuth values you set),
|
||||
it's just for your own bookkeeping.
|
||||
|
||||
At least two images are required to generate a solar wallpaper.
|
||||
|
||||
## 4. Generate
|
||||
|
||||
At the bottom of the window:
|
||||
|
||||
1. **Output file** — where to save the `.heic` file. Defaults to
|
||||
`~/Desktop/MyDynamicWallpaper.heic`; click **Choose…** to pick somewhere
|
||||
else.
|
||||
2. **Quality** — HEIC compression quality from `0.1` (small file, more
|
||||
compression artifacts) to `1.0` (largest file, best quality). `0.9` is a
|
||||
good default.
|
||||
3. Click **Generate Wallpaper**.
|
||||
|
||||
The first time you do this on a fresh install, there's a short one-time delay
|
||||
(a few seconds) while the app compiles its native helper tool in the
|
||||
background — see [BUILD.md](BUILD.md#3-building-the-native-helper-heic-builder)
|
||||
for what's happening. Every generation after that is fast.
|
||||
|
||||
Progress is streamed into the log box at the very bottom as each image is
|
||||
loaded and the file is written. If something goes wrong (a missing image, an
|
||||
unwritable output path, a missing Swift toolchain), you'll see an error
|
||||
dialog and the reason in the log.
|
||||
|
||||
## 5. Set as Desktop Picture
|
||||
|
||||
Once generation finishes successfully, the **Set as Desktop Picture** button
|
||||
becomes active. Clicking it applies the file you just built as your current
|
||||
desktop picture on all displays, via the same mechanism as System Settings.
|
||||
This only changes your desktop picture setting — it's fully reversible from
|
||||
System Settings → Wallpaper at any time, the same as picking any other
|
||||
wallpaper.
|
||||
|
||||
You can also just leave the generated `.heic` file where you saved it and add
|
||||
it manually later: open **System Settings → Wallpaper → Add Photo**, or
|
||||
double-click the file in Finder and choose **Set Desktop Picture**.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause / fix |
|
||||
|---|---|
|
||||
| "The Swift compiler (swiftc) was not found" | Install Xcode Command Line Tools: `xcode-select --install`, then try again. |
|
||||
| Generate fails immediately with a decode error | One of your source images is in a format Apple's ImageIO can't read, or the file is corrupt. Try re-exporting it as PNG or JPEG. |
|
||||
| Wallpaper doesn't animate after setting it | Make sure you're on macOS Mojave (10.14) or later, and that "Dynamic Desktop" hasn't been disabled — check System Settings → Wallpaper on the image you just set; it should show a Light/Dark/Dynamic selector under the thumbnail. |
|
||||
| Images look stretched or misaligned when swapping | Your light/dark or solar images have different pixel dimensions. Re-export them all at the same resolution. |
|
||||
Reference in New Issue
Block a user