Skip to content

feat: Google Motion Photo support for every adapter - #83

Merged
ildyria merged 2 commits into
LycheeOrg:masterfrom
mlapaglia:feat/google-motion-photos
Oct 19, 2026
Merged

ildyria merged 2 commits into
LycheeOrg:masterfrom
mlapaglia:feat/google-motion-photos

Conversation

@mlapaglia

Copy link
Copy Markdown

Report the video embedded in Google Motion Photos as MicroVideoOffset (its length in bytes from the end of the file) for every adapter, in both formats:

  • legacy GCamera:MicroVideoOffset (Android 10 and earlier), which only the Exiftool mapper read until now;
  • Motion Photo 1.0 (GCamera:MotionPhoto with a Container:Directory, Android 11 and later, Ultra HDR files with a gain map included), which no adapter read.

Requested on LycheeOrg/Lychee#4900, where this was first written inside Lychee.

How

The new PHPExif\MotionPhoto classes read the JPEG itself instead of mapping adapter output. Exiftool's JSON flattens Container:Directory into parallel lists, and they stop lining up when an item has no Item:Length; the primary item of real Pixel files has none. The Native and ImageMagick adapters don't expose the directory at all.

  • JpegXmpReader walks the header from SOI to SOS and returns the standard XMP packet, checking every segment length against the file size.
  • MotionPhotoXmp parses it with ext-xml, matching namespaces by URI. External entities are not loaded.
  • MotionPhotoLocator::locate(string $path): int takes the length of the MotionPhoto container item, or the legacy offset. A length is only accepted if an MP4 ftyp box starts where it points.
  • If the file says MotionPhoto="1" but no length is valid, the locator scans for the first plausible ftyp box after the primary image's end-of-image marker. It reads in 64 KiB chunks, so memory use stays constant.
  • Reader::read() calls the locator only when the adapter returned no MicroVideoOffset, and only sets the value when a video is found. Existing Exiftool results are kept. Non-JPEG files are rejected after reading two bytes.

The locator can also be used on its own. Resources/doc/usage.md has a short section on both uses.

Tests

  • 61 new tests under tests/PHPExif/MotionPhoto/ (group motionphoto):
    • the JPEG walk, including truncated and oversized segments and fill bytes;
    • the XMP parser, including attribute and element syntax, prefixes matched by URI, malformed XML and external entities;
    • the locator, including an ftyp look-alike inside image data, boundaries split across chunks, a missing end-of-image marker, and a 20 MB constant-memory check;
    • Reader with the Native, ImageMagick and Exiftool adapters on two new samples, plus checks that an adapter-provided value is kept and that videos are left alone.
  • New samples: tests/files/motion_photo_v1.jpg and motion_photo_ultrahdr.jpg, about 200 KB each. They are mongolia.jpeg with a motion photo XMP packet, a 1 s H.264 test clip and, for Ultra HDR, a real Pixel gain map.
  • Locally on PHP 8.4:
    • phpstan analyze passes with no errors.
    • check-code-style and validate-files pass.
    • PHPUnit: 464 tests. The one failure, ExifTest::testAdapterConsistency (a GPS mismatch between ImageMagick and Native on dsc_0003.jpg), also fails on master here.

The Exiftool cases only pass in a full run, like the existing ExiftoolTest. When run alone, the proc_open override in ExiftoolProcOpenTest makes the adapter report that it can't open exiftool.

🤖 Generated with Claude Code

Read the legacy MicroVideoOffset and Motion Photo 1.0 Container:Directory from the JPEG itself and report the video length as MicroVideoOffset from Reader::read(), so Native, ImageMagick and exiftool behave the same. The scan fallback starts after the primary image's EOI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.80838% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.51%. Comparing base (94f2670) to head (f1a00d1).
⚠️ Report is 13 commits behind head on master.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Safe 2.x has no ftell wrapper and Safe 3.x only wraps xml_parser_set_option for PHP 8.4, which broke the PHP 8.2 and 8.3 test jobs. Use the native functions and handle the ftell failure explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ildyria
ildyria merged commit 05f9945 into LycheeOrg:master Oct 19, 2026
8 of 10 checks passed
mlapaglia added a commit to mlapaglia/Lychee that referenced this pull request Oct 19, 2026
php-exif 1.5.0 now locates the video of Google Motion Photos for every reader (LycheeOrg/php-exif#83). Bump the dependency, drop Lychee's own reader and its unit tests, and let the backfill command call the php-exif locator.

Spec impact: ADR-100-02 supersedes ADR-100-01, Q-100-08 records the decision, and FR-100-01, the services catalogue, test strategy, knowledge map and upload lifecycle now point at php-exif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants