Initial Application

This commit is contained in:
2026-07-11 22:52:23 -04:00
parent c2bf076dc4
commit 4ee6ebb18a
21 changed files with 2194 additions and 1 deletions

133
docs/BUILD.md Normal file
View 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
View 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
View 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. |