1MB partial HEIC reads are not sufficient for sips to convert to JPEG.
Revert to full file pull for HEIC thumbnails. Files are cached permanently
so each HEIC is only transferred once — subsequent launches skip already-cached files.
For HEIC photos: read first 1MB (thumbnail track is near file start),
then use sips -getProperty thumbnail to extract the embedded JPEG
thumbnail without decoding the full image. Falls back to full resample
if thumbnail extraction fails.
For JPEG/PNG: unchanged, 512KB partial read + resample.
This avoids transferring 3-5MB per HEIC photo just for a thumbnail.
HEIC is not front-loaded — partial 512KB reads produce broken output.
batch-thumbs now pulls full file for HEIC/HEIF, partial for JPEG/PNG.
sips: add --setProperty format jpeg to all thumbnail conversions so
HEIC files are always converted to JPEG. SkiaImage on JVM desktop
cannot decode HEIC natively — JPEG is required.
batch-thumbs pulls multiple files in one Python process session,
amortising the pymobiledevice3 startup cost. Not yet wired into the
app — thumbnails load lazily per-cell and cache permanently.
Known limitation: first load of iPhone thumbnails is slow (~2-3s each)
because each pull spawns a new Python process. After first view they
are instant from the on-disk cache.
Add pull-thumb command to helper script that uses afc.fread() to read
only the first 512KB of each photo. EXIF thumbnails are in the first
few KB so sips can generate a resized thumbnail from the partial file.
Falls back to full afc.pull() if fread is unavailable.
IosConnector.pullToFile() uses pull-thumb for thumbnail requests.
Full afc.pull() still used for actual photo import.
pymobiledevice3 AfcService.stat() returns 'datetime' as a tuple
(year, month, day, hour, minute, second, microsecond) not 'st_mtime'.
Parse it directly into a datetime object and emit as ISO string.
iOS helper: log raw stat keys+values for first file to stderr.
IosConnector: already logs stderr at WARN level.
logback.xml: print DEBUG+ for photophetch package to console so
stat output is visible when running ./gradlew run.
Threshold: >1e15 = nanoseconds, >1e9 = seconds. Previous threshold of
1e18 was too high for nanosecond timestamps in the year 2025 range
(which are ~1.75e18, so actually fine). Also revert pull to use
afc.pull() since it exists in this pymobiledevice3 version.
Date: AFC st_mtime is nanoseconds since epoch. Fix threshold check:
values > 1e18 are nanoseconds, > 1e9 are seconds. Dec 31 bug was
caused by mtime being treated as seconds when it was nanoseconds,
giving a timestamp near epoch (1970 = Dec 31 local time).
Pull: use get_file_contents() which is the correct async AFC method
(not pull/afc.open which don't exist in this pymobiledevice3 version).
The helper script now uses asyncio.run() for all pymobiledevice3 calls
since create_using_usbmux() and AfcService methods are async in newer
versions.
IosConnector: auto-discovers the pipx venv Python at
~/Library/Application Support/pipx/venvs/pymobiledevice3/bin/python3
instead of using system python3 (which doesn't have the module).
Falls back to ~/.local/pipx/... paths too.
AppConfig python3Path acts as an override if auto-discovery fails.
gphoto2 stopped working on iOS 17+ (Apple tightened PTP access).
New approach uses pymobiledevice3 via AFC protocol — the same protocol
Finder uses to browse the iPhone.
- photophetch-ios-helper.py: bundled Python script (in JAR resources)
that calls pymobiledevice3 AFC to list-devices, list-photos, pull, delete
- IosConnector: rewritten to shell out to the helper script instead of
gphoto2. Extracts the script from JAR to a temp file on first use.
- AppConfig: gphoto2Path → python3Path
- AppState: IosConnector now receives python3Path
Prerequisites: brew install pipx && pipx install pymobiledevice3