From 26cb6f8ad197569bd3434b8d223de5129c7e6d61 Mon Sep 17 00:00:00 2001 From: Dichen Zhang Date: Fri, 2 Oct 2026 00:35:40 +0000 Subject: [PATCH 1/3] Fix Ubuntu GitHub Actions pre-submit build and update API doc check paths - Use absl::string_view in GoogleMotionPhotoProvider::Identify and LegacyMicroVideoProvider::Identify where absl::StrContains is called, fixing compilation against Debian/Ubuntu system libabsl-dev (where absl::string_view is absl::debian3::string_view). - Respect LIBMOTIONPHOTO_USE_SYSTEM_DEPS when resolving absl in lib/core/CMakeLists.txt and guard optional absl::log_internal_check_op with $<$:...>. - Avoid installing system abseil in fetchcontent CI jobs so FetchContent is properly exercised. - Update Java API doc check workflow and script paths to lib/java/** and harden workflows for Zizmor compliance (explicit permissions, SHA-pinned checkout action, and env variable step inputs). --- .github/workflows/build_and_test.yml | 13 +++++++++---- .github/workflows/check_api_doc.yml | 9 ++++++--- lib/core/CMakeLists.txt | 17 ++++++++++------- .../google_motion_photo_provider.cc | 4 ++-- .../legacy_micro_video_provider.cc | 4 ++-- tools/check_api_doc.sh | 6 +++--- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b3d9cbd..233bdd9 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -20,6 +20,9 @@ on: pull_request: branches: [ main, master ] +permissions: + contents: read + jobs: build-and-test: name: Build & Test (${{ matrix.os }}, deps=${{ matrix.deps_mode }}) @@ -32,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install dependencies (Ubuntu - system deps) if: runner.os == 'Linux' && matrix.deps_mode == 'system' @@ -44,7 +47,7 @@ jobs: if: runner.os == 'Linux' && matrix.deps_mode == 'fetchcontent' run: | sudo apt-get update - sudo apt-get install -y cmake build-essential libprotobuf-dev protobuf-compiler libabsl-dev pkg-config + sudo apt-get install -y cmake build-essential libprotobuf-dev protobuf-compiler pkg-config - name: Install dependencies (macOS - system deps) if: runner.os == 'macOS' && matrix.deps_mode == 'system' @@ -56,12 +59,14 @@ jobs: if: runner.os == 'macOS' && matrix.deps_mode == 'fetchcontent' run: | brew update - brew install cmake protobuf abseil pkg-config + brew install cmake protobuf pkg-config - name: Run Build + env: + DEPS_MODE: ${{ matrix.deps_mode }} run: | chmod +x build.sh test.sh - if [ "${{ matrix.deps_mode }}" = "fetchcontent" ]; then + if [ "$DEPS_MODE" = "fetchcontent" ]; then ./build.sh -DLIBMOTIONPHOTO_USE_SYSTEM_DEPS=OFF -DLIBMOTIONPHOTO_DOWNLOAD_DEPS=ON else ./build.sh -DLIBMOTIONPHOTO_USE_SYSTEM_DEPS=ON diff --git a/.github/workflows/check_api_doc.yml b/.github/workflows/check_api_doc.yml index 5df80ae..7a11686 100644 --- a/.github/workflows/check_api_doc.yml +++ b/.github/workflows/check_api_doc.yml @@ -17,15 +17,18 @@ name: Java API Doc Sync Check on: pull_request: paths: - - 'java/com/google/libmotionphoto/motionphoto/**' - - 'java/java_api_description.md' + - 'lib/java/src/main/java/com/google/libmotionphoto/motionphoto/**' + - 'lib/java/java_api_description.md' + +permissions: + contents: read jobs: check-doc: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index f3a5755..7ffde36 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -26,9 +26,13 @@ find_package(Protobuf CONFIG QUIET) if (NOT Protobuf_FOUND) find_package(Protobuf REQUIRED) endif() -find_package(absl CONFIG QUIET) -if (NOT absl_FOUND) - find_package(absl QUIET) +include(cmake/Dependencies.cmake) + +if (LIBMOTIONPHOTO_USE_SYSTEM_DEPS) + find_package(absl CONFIG QUIET) + if (NOT absl_FOUND) + find_package(absl QUIET) + endif() endif() if (NOT TARGET absl::strings) include(FetchContent) @@ -36,6 +40,7 @@ if (NOT TARGET absl::strings) abseil_cpp GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git GIT_TAG 20240116.2 + GIT_SHALLOW TRUE ) set(ABSL_PROPAGATE_CXX_STANDARD ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(abseil_cpp) @@ -58,8 +63,6 @@ if (NOT TARGET protobuf::protoc AND PROTOC_BINARY) ) endif() -include(cmake/Dependencies.cmake) - if (LIBHEIF_FOUND) add_compile_definitions(MOTION_PHOTO_HEIC_SUPPORT=1) endif() @@ -230,7 +233,7 @@ if (ANDROID) motion_photo_extractor_lib motion_photo protobuf::libprotobuf-lite - absl::log_internal_check_op + $<$:absl::log_internal_check_op> ) target_link_options(motion_photo_jni PRIVATE "-Wl,-z,max-page-size=16384") else() @@ -244,7 +247,7 @@ else() motion_photo_extractor_lib motion_photo protobuf::libprotobuf-lite - absl::log_internal_check_op + $<$:absl::log_internal_check_op> ${JNI_LIBRARIES} ) endif() diff --git a/lib/core/motion_photo/src/motion_photo/google_motion_photo_provider.cc b/lib/core/motion_photo/src/motion_photo/google_motion_photo_provider.cc index 21fe11f..1357890 100644 --- a/lib/core/motion_photo/src/motion_photo/google_motion_photo_provider.cc +++ b/lib/core/motion_photo/src/motion_photo/google_motion_photo_provider.cc @@ -52,8 +52,8 @@ bool GoogleMotionPhotoProvider::Identify(const RawMetadataBlock& block) { } if (block.type == "XMP" && block.format_identifier == "standard.xmp") { - std::string_view xml(reinterpret_cast(block.bytes.data()), - block.bytes.size()); + absl::string_view xml(reinterpret_cast(block.bytes.data()), + block.bytes.size()); return (absl::StrContains(xml, "http://ns.google.com/photos/1.0/camera/") || absl::StrContains(xml, "http://ns.google.com/photos/1.0/container/")) && diff --git a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc index 082ce52..257eacb 100644 --- a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc +++ b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc @@ -53,8 +53,8 @@ int64_t ExtractInt64Attr(std::string_view xml, const std::string& attr_name) { bool LegacyMicroVideoProvider::Identify(const RawMetadataBlock& block) { if (block.type == "XMP" && block.format_identifier == "standard.xmp") { - std::string_view xml(reinterpret_cast(block.bytes.data()), - block.bytes.size()); + absl::string_view xml(reinterpret_cast(block.bytes.data()), + block.bytes.size()); return absl::StrContains(xml, "MicroVideo"); } if (block.format_identifier == "container.trailer") { diff --git a/tools/check_api_doc.sh b/tools/check_api_doc.sh index ad65953..1409f3b 100755 --- a/tools/check_api_doc.sh +++ b/tools/check_api_doc.sh @@ -32,12 +32,12 @@ fi JAVA_CHANGED=false DOC_CHANGED=false -JAVA_PATH_PATTERN="java/com/google/libmotionphoto/motionphoto/[^/]+\.java" -DOC_PATH="java/java_api_description.md" +JAVA_PATH_PATTERN="lib/java/src/main/java/com/google/libmotionphoto/motionphoto/[^/]+\.java" +DOC_PATH="lib/java/java_api_description.md" for file in $CHANGED_FILES; do # Strip arbitrary prefixes to make path relative to repository root - clean_file=$(echo "$file" | sed -E 's|^(.*[/\\])?(java/.*)|\2|') + clean_file=$(echo "$file" | sed -E 's|^(.*[/\\])?(lib/java/.*)|\2|') if [[ "$clean_file" =~ $JAVA_PATH_PATTERN ]]; then JAVA_CHANGED=true echo "Detected Java API change: $clean_file" From 26e3a468d96bac8cf518d11f20e331b246198add Mon Sep 17 00:00:00 2001 From: Dichen Zhang Date: Fri, 2 Oct 2026 01:34:38 +0000 Subject: [PATCH 2/3] [libmotionphoto] Optimize JPEG motion photo detection and trailer parsing performance. - Early-terminate JPEG scanner at SOS marker once XMP parses video trailer bounds, avoiding multi-megabyte entropy scan. - Parse video trailer bounds (Item:Length / MicroVideoOffset) directly from XMP in JpegContainerSplitter. - Keep only a 64-byte prefix in RawMetadataBlock::bytes while preserving total_payload_size to eliminate 20-120 MB RAM allocations and copies for motion photo video trailers. - Use lazy FdDataSource (pread in 64 KB chunks on demand over full file range) across C++ (ParseMotionPhotoFromFile/Fd, ExtractPrimaryImageFromFile, ExtractVideoTrackFromFile) and JNI (MetadataEngineJni.parseMetadata and parseMetadataFd). Tested: - ./build.sh && ./test.sh (57/57 unit tests pass) --- .../includes/motion_photo/metadata_block.h | 3 + .../motion_photo/jpeg_container_splitter.cc | 148 ++++++++- .../src/motion_photo/libmotionphoto_api.cc | 235 +++++++++++--- .../src/motion_photo_jni/motion_photo_jni.cc | 296 +++++++++++------- .../legacy_micro_video_provider.cc | 3 +- 5 files changed, 518 insertions(+), 167 deletions(-) diff --git a/lib/core/motion_photo/includes/motion_photo/metadata_block.h b/lib/core/motion_photo/includes/motion_photo/metadata_block.h index fb6a3c8..31973b2 100644 --- a/lib/core/motion_photo/includes/motion_photo/metadata_block.h +++ b/lib/core/motion_photo/includes/motion_photo/metadata_block.h @@ -27,6 +27,9 @@ struct RawMetadataBlock { std::string format_identifier; std::vector bytes; size_t offset = 0; + // If non-zero, represents the full size of the underlying payload block + // in cases where `bytes` only buffers a prefix (e.g. for container trailers). + size_t total_payload_size = 0; }; } // namespace motion_photo diff --git a/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc b/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc index 855f138..6f85936 100644 --- a/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc +++ b/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc @@ -13,18 +13,24 @@ // limitations under the License. #include +#include #include #include #include #include +#include #include +#include "absl/strings/match.h" #include "image_io/base/data_source.h" #include "image_io/base/message_handler.h" #include "image_io/jpeg/jpeg_marker.h" #include "image_io/jpeg/jpeg_scanner.h" +#include "image_io/jpeg/jpeg_segment.h" #include "image_io/jpeg/jpeg_segment_processor.h" #include "motion_photo/container_splitter.h" +#include "motion_photo/metadata_block.h" +#include "motion_photo/motion_photo.h" namespace libmotionphoto { namespace motion_photo { @@ -57,13 +63,14 @@ bool CopyFromDataSource(image_io::DataSource* source, size_t start, class JpegContainerSplitter : public ContainerSplitter, public image_io::JpegSegmentProcessor { public: - JpegContainerSplitter(image_io::MessageHandler* message_handler) + explicit JpegContainerSplitter(image_io::MessageHandler* message_handler) : message_handler_(message_handler) {} std::vector Split(image_io::DataSource* data_source, size_t file_size) override { blocks_.clear(); eoi_location_ = 0; + xmp_video_length_ = 0; image_io::JpegScanner scanner(message_handler_); @@ -71,22 +78,38 @@ class JpegContainerSplitter : public ContainerSplitter, flags.set(image_io::JpegMarker::kAPP0); flags.set(image_io::JpegMarker::kAPP1); flags.set(image_io::JpegMarker::kAPP2); + flags.set(image_io::JpegMarker::kSOS); flags.set(image_io::JpegMarker::kEOI); scanner.UpdateInterestingMarkerFlags(flags); scanner.Run(data_source, this); - if (eoi_location_ > 0 && eoi_location_ < file_size) { - size_t start = eoi_location_ + 2; // skip EOI marker (0xFFD9) - if (start < file_size) { - RawMetadataBlock block; - block.type = "PROPRIETARY"; - block.format_identifier = "container.trailer"; - block.offset = start; - if (CopyFromDataSource(data_source, start, file_size - start, - &block.bytes)) { - blocks_.push_back(block); - } + size_t trailer_start = 0; + size_t trailer_length = 0; + + if (xmp_video_length_ > 0 && xmp_video_length_ < file_size) { + trailer_start = file_size - xmp_video_length_; + trailer_length = xmp_video_length_; + } else if (eoi_location_ > 0 && eoi_location_ < file_size) { + trailer_start = eoi_location_ + 2; // skip EOI marker (0xFFD9) + if (trailer_start < file_size) { + trailer_length = file_size - trailer_start; + } + } + + if (trailer_start > 0 && trailer_start < file_size && trailer_length > 0) { + RawMetadataBlock block; + block.type = "PROPRIETARY"; + block.format_identifier = "container.trailer"; + block.offset = trailer_start; + block.total_payload_size = trailer_length; + // Copy only header bytes needed for format verification (e.g. ftyp, moov) + // to avoid allocating/copying large video payloads (up to 120 MB) into + // RAM. + size_t bytes_to_copy = std::min(trailer_length, 64); + if (CopyFromDataSource(data_source, trailer_start, bytes_to_copy, + &block.bytes)) { + blocks_.push_back(std::move(block)); } } return blocks_; @@ -110,12 +133,28 @@ class JpegContainerSplitter : public ContainerSplitter, segment.GetPayloadDataLocation(), "http://ns.adobe.com/xap/1.0/\0")) { ExtractBlock(segment, "XMP", "standard.xmp", 29); // skip namespace + if (!blocks_.empty() && + blocks_.back().format_identifier == "standard.xmp") { + std::string_view xml( + reinterpret_cast(blocks_.back().bytes.data()), + blocks_.back().bytes.size()); + size_t length = ParseVideoLengthFromXmp(xml); + if (length > 0) { + xmp_video_length_ = length; + } + } } } else if (marker.GetType() == image_io::JpegMarker::kAPP2) { if (segment.BytesAtLocationStartWith(segment.GetPayloadDataLocation(), "ICC_PROFILE\0")) { ExtractBlock(segment, "ICC", "standard.icc", 14); // skip signature and chunk info } + } else if (marker.GetType() == image_io::JpegMarker::kSOS) { + if (xmp_video_length_ > 0) { + // All metadata segments precede SOS. If video trailer bounds are + // already known from XMP, stop scanning entropy-coded image data. + scanner->SetDone(); + } } else if (marker.GetType() == image_io::JpegMarker::kEOI) { eoi_location_ = segment.GetBegin(); scanner->SetDone(); @@ -125,6 +164,90 @@ class JpegContainerSplitter : public ContainerSplitter, void Finish(image_io::JpegScanner* scanner) override {} private: + static size_t ParseVideoLengthFromXmp(std::string_view xml) { + // 1. Check Container:Directory Item:Length where + // Item:Semantic="MotionPhoto" Handles both Item:Semantic="MotionPhoto" + // Item:Length="..." and reversed attribute order. + size_t motion_photo_pos = xml.find("MotionPhoto"); + while (motion_photo_pos != std::string_view::npos) { + // Find enclosing tag bounds '<...>' + size_t tag_start = xml.rfind('<', motion_photo_pos); + size_t tag_end = xml.find('>', motion_photo_pos); + if (tag_start != std::string_view::npos && + tag_end != std::string_view::npos && tag_start < tag_end) { + std::string_view tag = xml.substr(tag_start, tag_end - tag_start + 1); + absl::string_view absl_tag(tag.data(), tag.size()); + if (absl::StrContains(absl_tag, "Item:Length") || + absl::StrContains(absl_tag, "Length")) { + size_t len_attr = tag.find("Length=\""); + size_t val_start = (len_attr != std::string_view::npos) + ? len_attr + 8 + : std::string_view::npos; + if (val_start == std::string_view::npos) { + len_attr = tag.find("Length='"); + if (len_attr != std::string_view::npos) { + val_start = len_attr + 8; + } + } + if (val_start != std::string_view::npos) { + char quote = tag[val_start - 1]; + size_t val_end = tag.find(quote, val_start); + if (val_end != std::string_view::npos) { + std::string val_str(tag.substr(val_start, val_end - val_start)); + try { + int64_t len = std::stoll(val_str); + if (len > 0) return static_cast(len); + } catch (...) { + } + } + } + } + } + motion_photo_pos = xml.find("MotionPhoto", motion_photo_pos + 1); + } + + // 2. Check GCamera:MicroVideoOffset / Camera:MicroVideoOffset + size_t offset_pos = xml.find("MicroVideoOffset=\""); + size_t val_start = (offset_pos != std::string_view::npos) + ? offset_pos + 18 + : std::string_view::npos; + if (val_start == std::string_view::npos) { + offset_pos = xml.find("MicroVideoOffset='"); + if (offset_pos != std::string_view::npos) { + val_start = offset_pos + 18; + } + } + if (val_start != std::string_view::npos) { + char quote = xml[val_start - 1]; + size_t val_end = xml.find(quote, val_start); + if (val_end != std::string_view::npos) { + std::string val_str(xml.substr(val_start, val_end - val_start)); + try { + int64_t len = std::stoll(val_str); + if (len > 0) return static_cast(len); + } catch (...) { + } + } + } + + // Check XML element: <(GCamera|Camera):MicroVideoOffset>... + size_t elem_pos = xml.find(":MicroVideoOffset>"); + if (elem_pos != std::string_view::npos) { + size_t text_start = elem_pos + 18; + size_t text_end = xml.find('<', text_start); + if (text_end != std::string_view::npos) { + std::string val_str(xml.substr(text_start, text_end - text_start)); + try { + int64_t len = std::stoll(val_str); + if (len > 0) return static_cast(len); + } catch (...) { + } + } + } + + return 0; + } + void ExtractBlock(const image_io::JpegSegment& segment, std::string_view type, std::string_view format_id, size_t skip_bytes) { size_t start = segment.GetPayloadDataLocation() + skip_bytes; @@ -145,6 +268,7 @@ class JpegContainerSplitter : public ContainerSplitter, image_io::MessageHandler* message_handler_; std::vector blocks_; size_t eoi_location_ = 0; + size_t xmp_video_length_ = 0; }; } // namespace diff --git a/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc b/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc index 5864c1b..87a8bbd 100644 --- a/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc +++ b/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc @@ -15,23 +15,30 @@ #include "libmotionphoto_api.h" #include +#include #include +#include #include #include +#include #include #include +#include +#include #include #include #include #include +#include "image_io/base/data_destination.h" #include "image_io/base/data_range.h" #include "image_io/base/data_segment.h" #include "image_io/base/data_segment_data_source.h" +#include "image_io/base/data_source.h" #include "image_io/base/message_handler.h" #include "image_io/base/message_writer.h" -#include "image_io/utils/file_utils.h" +#include "image_io/base/types.h" #include "image_io/utils/string_outputter.h" #include "image_io/utils/string_outputter_message_writer.h" #include "motion_photo/camera_metadata.h" @@ -94,6 +101,130 @@ void PopulateNativeMetadata(const ::libmotionphoto::motion_photo::MotionPhoto& p } } +// Lazy DataSource backed by pread() on an open file descriptor. +// Only fetches requested chunks (e.g. initial 64-128 KB headers) into RAM, +// avoiding full-file buffering in memory. +class FdDataSource : public ::photos_editing_formats::image_io::DataSource { + public: + FdDataSource(int fd, int64_t base_offset, int64_t total_length, + bool owns_fd = false) + : fd_(fd), + base_offset_(base_offset), + total_length_(total_length), + owns_fd_(owns_fd) {} + + ~FdDataSource() override { + if (owns_fd_ && fd_ >= 0) { + close(fd_); + fd_ = -1; + } + } + + void Reset() override { current_data_segment_.reset(); } + + std::shared_ptr<::photos_editing_formats::image_io::DataSegment> + GetDataSegment(size_t begin, size_t min_size) override { + if (current_data_segment_ && current_data_segment_->Contains(begin)) { + size_t remaining = current_data_segment_->GetEnd() - begin; + if (remaining >= min_size) { + return current_data_segment_; + } + } + current_data_segment_ = Read(begin, min_size); + return current_data_segment_; + } + + TransferDataResult TransferData( + const ::photos_editing_formats::image_io::DataRange& data_range, + size_t best_size, + ::photos_editing_formats::image_io::DataDestination* data_destination) + override { + bool data_transferred = false; + ::photos_editing_formats::image_io::DataDestination::TransferStatus status = + ::photos_editing_formats::image_io::DataDestination::kTransferDone; + if (data_destination && data_range.IsValid() && fd_ >= 0) { + size_t min_size = std::min(data_range.GetLength(), best_size); + if (current_data_segment_ && + current_data_segment_->GetLength() >= min_size && + current_data_segment_->GetDataRange().Contains(data_range)) { + status = data_destination->Transfer(data_range, *current_data_segment_); + data_transferred = true; + } else { + size_t chunk_size = std::max(min_size, static_cast(64 * 1024)); + for (size_t begin = data_range.GetBegin(); begin < data_range.GetEnd(); + begin += chunk_size) { + size_t end = std::min(data_range.GetEnd(), begin + chunk_size); + auto data_segment = Read(begin, end - begin); + if (data_segment && data_segment->GetLength() > 0) { + status = data_destination->Transfer(data_segment->GetDataRange(), + *data_segment); + data_transferred = true; + } + if (status != ::photos_editing_formats::image_io::DataDestination:: + kTransferOk || + !data_segment || data_segment->GetLength() == 0) { + break; + } + } + } + } + if (data_transferred) { + return status == ::photos_editing_formats::image_io::DataDestination:: + kTransferError + ? kTransferDataError + : kTransferDataSuccess; + } + return data_destination ? kTransferDataNone : kTransferDataError; + } + + private: + std::shared_ptr<::photos_editing_formats::image_io::DataSegment> Read( + size_t begin, size_t min_size) { + if (fd_ < 0 || begin >= static_cast(total_length_)) { + return nullptr; + } + size_t chunk_size = std::max(min_size, static_cast(64 * 1024)); + size_t to_read = + std::min(chunk_size, static_cast(total_length_) - begin); + if (to_read == 0) { + return nullptr; + } + + auto buffer = + std::make_shared>( + to_read); + size_t total_bytes_read = 0; + while (total_bytes_read < to_read) { + ssize_t bytes = pread(fd_, buffer->data() + total_bytes_read, + to_read - total_bytes_read, + base_offset_ + begin + total_bytes_read); + if (bytes <= 0) break; + total_bytes_read += bytes; + } + + if (total_bytes_read == 0) { + return nullptr; + } + + auto segment = ::photos_editing_formats::image_io::DataSegment::Create( + ::photos_editing_formats::image_io::DataRange(begin, + begin + total_bytes_read), + buffer->data(), + ::photos_editing_formats::image_io::DataSegment:: + BufferDispositionPolicy::kDontDelete); + return std::shared_ptr<::photos_editing_formats::image_io::DataSegment>( + segment.get(), + [segment, buffer](::photos_editing_formats::image_io::DataSegment*) {}); + } + + int fd_ = -1; + int64_t base_offset_ = 0; + int64_t total_length_ = 0; + bool owns_fd_ = false; + std::shared_ptr<::photos_editing_formats::image_io::DataSegment> + current_data_segment_; +}; + } // namespace // ========================================================================= @@ -117,15 +248,20 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, ::libmotionphoto::motion_photo::FileType file_type = ::libmotionphoto::motion_photo::GetFileTypeFromFileName(filepath); - std::shared_ptr<::photos_editing_formats::image_io::DataSegment> data_segment = - ::photos_editing_formats::image_io::ReadEntireFile(filepath, &message_handler); - if (!data_segment || data_segment->GetLength() == 0) { + int fd = open(filepath.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return false; + } + struct stat st; + if (fstat(fd, &st) != 0 || st.st_size <= 0) { + close(fd); return false; } + int64_t file_size = st.st_size; - ::photos_editing_formats::image_io::DataSegmentDataSource data_source(data_segment); + FdDataSource data_source(fd, /*base_offset=*/0, file_size, /*owns_fd=*/true); ::libmotionphoto::motion_photo::MetadataCollection collection = - engine.Parse(&data_source, data_segment->GetLength(), file_type); + engine.Parse(&data_source, file_size, file_type); bool is_mp = engine.IsMotionPhoto(collection); @@ -151,7 +287,7 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, if (::libmotionphoto::motion_photo::IsHeif(file_type)) { ::libmotionphoto::motion_photo::MotionPhotoHeifInfoBuilder heif_builder(&message_handler); - if (heif_builder.Build(&data_source, data_segment->GetLength())) { + if (heif_builder.Build(&data_source, file_size)) { const auto& xmp_range = heif_builder.GetXmpStringRange(); if (xmp_range.GetLength() > 0) { std::shared_ptr<::photos_editing_formats::image_io::DataSegment> xmp_segment = @@ -197,21 +333,30 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, MotionPhotoMetadata* out_metadata, MessageCallback callback) { - if (fd < 0 || length <= 0) { + if (fd < 0 || offset < 0 || length <= 0) { return false; } - - std::vector buffer(length); - size_t bytes_read = 0; - while (bytes_read < static_cast(length)) { - ssize_t r = pread(fd, buffer.data() + bytes_read, - length - bytes_read, offset + bytes_read); - if (r <= 0) break; - bytes_read += r; - } - if (bytes_read < static_cast(length)) { + if (static_cast(offset) > + std::numeric_limits::max() - static_cast(length)) { return false; } + struct stat st; + if (fstat(fd, &st) == 0 && st.st_size >= 0) { + if (offset + length > st.st_size) { + return false; + } + } + + // Read a small 12-byte header to sniff file type without buffering entire + // length. + ::libmotionphoto::motion_photo::FileType file_type = + ::libmotionphoto::motion_photo::FileType::kJpeg; + char header_buf[12]; + ssize_t header_read = pread(fd, header_buf, sizeof(header_buf), offset); + if (header_read >= 12 && header_buf[4] == 'f' && header_buf[5] == 't' && + header_buf[6] == 'y' && header_buf[7] == 'p') { + file_type = ::libmotionphoto::motion_photo::FileType::kHeic; + } ::photos_editing_formats::image_io::MessageHandler message_handler; if (callback) { @@ -225,20 +370,10 @@ bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, ::libmotionphoto::motion_photo::MetadataEngine engine(&message_handler); - ::libmotionphoto::motion_photo::FileType file_type = - ::libmotionphoto::motion_photo::FileType::kJpeg; - if (buffer.size() >= 12 && buffer[4] == 'f' && buffer[5] == 't' && - buffer[6] == 'y' && buffer[7] == 'p') { - file_type = ::libmotionphoto::motion_photo::FileType::kHeic; - } - - auto data_segment = ::photos_editing_formats::image_io::DataSegment::Create( - ::photos_editing_formats::image_io::DataRange(0, buffer.size()), buffer.data(), - ::photos_editing_formats::image_io::DataSegment::BufferDispositionPolicy::kDontDelete); - ::photos_editing_formats::image_io::DataSegmentDataSource data_source(data_segment); + FdDataSource data_source(fd, offset, length, /*owns_fd=*/false); ::libmotionphoto::motion_photo::MetadataCollection collection = - engine.Parse(&data_source, buffer.size(), file_type); + engine.Parse(&data_source, length, file_type); bool is_mp = engine.IsMotionPhoto(collection); @@ -264,7 +399,7 @@ bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, if (::libmotionphoto::motion_photo::IsHeif(file_type)) { ::libmotionphoto::motion_photo::MotionPhotoHeifInfoBuilder heif_builder(&message_handler); - if (heif_builder.Build(&data_source, buffer.size())) { + if (heif_builder.Build(&data_source, length)) { const auto& xmp_range = heif_builder.GetXmpStringRange(); if (xmp_range.GetLength() > 0) { std::shared_ptr<::photos_editing_formats::image_io::DataSegment> xmp_segment = @@ -445,14 +580,17 @@ int CheckMotionPhotoFromMemory(const uint8_t* data, size_t size, bool ExtractPrimaryImageFromFile(const std::string& input_filepath, const std::string& output_image_path, MessageCallback callback) { - ::libmotionphoto::motion_photo::MotionPhotoExtractorParams params; - params.motion_photo_file_name = input_filepath; - params.primary_image_file_name_output = output_image_path; - ::photos_editing_formats::image_io::StringOutputter outputter([callback](const std::string& msg) { - if (callback) callback(msg); - }); - int status = ::libmotionphoto::motion_photo::ExtractMotionPhoto(params, outputter); - return status == 0; + int fd = open(input_filepath.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) return false; + struct stat st; + if (fstat(fd, &st) != 0 || st.st_size <= 0) { + close(fd); + return false; + } + bool res = + ExtractPrimaryImageFromFd(fd, 0, st.st_size, output_image_path, callback); + close(fd); + return res; } bool ExtractPrimaryImageFromFd(int fd, int64_t offset, int64_t length, @@ -490,14 +628,17 @@ bool ExtractPrimaryImageFromMemory(const uint8_t* data, size_t size, bool ExtractVideoTrackFromFile(const std::string& input_filepath, const std::string& output_video_path, MessageCallback callback) { - ::libmotionphoto::motion_photo::MotionPhotoExtractorParams params; - params.motion_photo_file_name = input_filepath; - params.video_file_name_output = output_video_path; - ::photos_editing_formats::image_io::StringOutputter outputter([callback](const std::string& msg) { - if (callback) callback(msg); - }); - int status = ::libmotionphoto::motion_photo::ExtractMotionPhoto(params, outputter); - return status == 0; + int fd = open(input_filepath.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) return false; + struct stat st; + if (fstat(fd, &st) != 0 || st.st_size <= 0) { + close(fd); + return false; + } + bool res = + ExtractVideoTrackFromFd(fd, 0, st.st_size, output_video_path, callback); + close(fd); + return res; } bool ExtractVideoTrackFromFd(int fd, int64_t offset, int64_t length, diff --git a/lib/core/motion_photo/src/motion_photo_jni/motion_photo_jni.cc b/lib/core/motion_photo/src/motion_photo_jni/motion_photo_jni.cc index c653a66..9f8f07b 100644 --- a/lib/core/motion_photo/src/motion_photo_jni/motion_photo_jni.cc +++ b/lib/core/motion_photo/src/motion_photo_jni/motion_photo_jni.cc @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include #include #include +#include #include #include #include @@ -28,13 +30,15 @@ #include #include +#include "image_io/base/data_destination.h" #include "image_io/base/data_segment.h" #include "image_io/base/data_segment_data_source.h" +#include "image_io/base/data_source.h" #include "image_io/base/message_handler.h" #include "image_io/base/types.h" #include "image_io/utils/file_utils.h" #include "image_io/utils/string_outputter.h" -#include "metadata_collection.pb.h" +#include "metadata_collection.pb.h" // NOLINT #include "motion_photo/camera_metadata.h" #include "motion_photo/google_motion_photo_provider.h" #include "motion_photo/metadata_engine.h" @@ -59,6 +63,185 @@ using libmotionphoto::jni::ScopedUtfChars; using libmotionphoto::jni::ThrowOutOfMemoryError; using libmotionphoto::jni::ThrowRuntimeException; +// Lazy random-access DataSource backed by pread() on an open file descriptor. +// Reads 64 KB chunks on demand over the full [0, total_length_) range without +// buffering the entire file in memory. +class FdDataSource : public libmotionphoto::image_io::DataSource { + public: + FdDataSource(int fd, int64_t base_offset, int64_t total_length) + : fd_(fd), base_offset_(base_offset), total_length_(total_length) {} + + ~FdDataSource() override = default; + + void Reset() override { current_data_segment_.reset(); } + + std::shared_ptr GetDataSegment( + size_t begin, size_t min_size) override { + if (current_data_segment_ && current_data_segment_->Contains(begin)) { + size_t remaining = current_data_segment_->GetEnd() - begin; + if (remaining >= min_size) { + return current_data_segment_; + } + } + current_data_segment_ = Read(begin, min_size); + return current_data_segment_; + } + + TransferDataResult TransferData( + const libmotionphoto::image_io::DataRange& data_range, size_t best_size, + libmotionphoto::image_io::DataDestination* data_destination) override { + namespace ii = libmotionphoto::image_io; + bool data_transferred = false; + ii::DataDestination::TransferStatus status = + ii::DataDestination::kTransferDone; + if (data_destination && data_range.IsValid() && fd_ >= 0) { + size_t min_size = std::min(data_range.GetLength(), best_size); + if (current_data_segment_ && + current_data_segment_->GetLength() >= min_size && + current_data_segment_->GetDataRange().Contains(data_range)) { + status = data_destination->Transfer(data_range, *current_data_segment_); + data_transferred = true; + } else { + size_t chunk_size = std::max(min_size, static_cast(64 * 1024)); + for (size_t begin = data_range.GetBegin(); begin < data_range.GetEnd(); + begin += chunk_size) { + size_t end = std::min(data_range.GetEnd(), begin + chunk_size); + auto data_segment = Read(begin, end - begin); + if (data_segment && data_segment->GetLength() > 0) { + status = data_destination->Transfer(data_segment->GetDataRange(), + *data_segment); + data_transferred = true; + } + if (status != ii::DataDestination::kTransferOk || !data_segment || + data_segment->GetLength() == 0) { + break; + } + } + } + } + if (data_transferred) { + return status == ii::DataDestination::kTransferError + ? kTransferDataError + : kTransferDataSuccess; + } + return data_destination ? kTransferDataNone : kTransferDataError; + } + + private: + std::shared_ptr Read(size_t begin, + size_t min_size) { + namespace ii = libmotionphoto::image_io; + if (fd_ < 0 || total_length_ <= 0 || + begin >= static_cast(total_length_)) { + return nullptr; + } + size_t chunk_size = std::max(min_size, static_cast(64 * 1024)); + size_t to_read = + std::min(chunk_size, static_cast(total_length_) - begin); + if (to_read == 0) { + return nullptr; + } + + auto buffer = std::make_shared>(to_read); + size_t total_bytes_read = 0; + while (total_bytes_read < to_read) { + ssize_t bytes = pread( + fd_, buffer->data() + total_bytes_read, to_read - total_bytes_read, + static_cast(base_offset_ + begin + total_bytes_read)); + if (bytes <= 0) { + if (bytes < 0 && errno == EINTR) continue; + break; + } + total_bytes_read += static_cast(bytes); + } + + if (total_bytes_read == 0) { + return nullptr; + } + + auto segment = ii::DataSegment::Create( + ii::DataRange(begin, begin + total_bytes_read), buffer->data(), + ii::DataSegment::BufferDispositionPolicy::kDontDelete); + return std::shared_ptr( + segment.get(), [segment, buffer](ii::DataSegment*) {}); + } + + int fd_ = -1; + int64_t base_offset_ = 0; + int64_t total_length_ = 0; + std::shared_ptr current_data_segment_; +}; + +jbyteArray ParseMetadataFromFd(JNIEnv* env, int fd, int64_t offset, + int64_t length, bool owns_fd) { + struct ScopedFd { + int fd; + bool owns; + ~ScopedFd() { + if (owns && fd >= 0) { + close(fd); + } + } + } fd_closer{fd, owns_fd}; + + if (env == nullptr || fd < 0 || offset < 0 || length < 0) { + return nullptr; + } + + struct stat sb; + if (fstat(fd, &sb) != 0 || sb.st_size <= 0) { + return nullptr; + } + + int64_t slice_length = length; + if (slice_length == 0) { + if (offset >= sb.st_size) { + return nullptr; + } + slice_length = sb.st_size - offset; + } else { + if (slice_length > sb.st_size || offset > sb.st_size - slice_length) { + return nullptr; + } + } + if (slice_length <= 0) { + return nullptr; + } + + namespace mp = libmotionphoto::motion_photo; + namespace ii = libmotionphoto::image_io; + + ii::MessageHandler message_handler; + FdDataSource data_source(fd, offset, slice_length); + + mp::MetadataEngine engine(&message_handler); + engine.RegisterProvider(std::make_unique()); + + mp::FileType file_type = mp::FileType::kJpeg; + mp::MetadataCollection collection = + engine.Parse(&data_source, static_cast(slice_length), file_type); + + for (int i = 0; i < collection.blocks_size(); ++i) { + if (collection.blocks(i).format_identifier() == "container.trailer") { + collection.mutable_blocks(i)->clear_raw_bytes(); + } + } + + std::vector proto_bytes(collection.ByteSizeLong()); + if (!collection.SerializeToArray(proto_bytes.data(), proto_bytes.size())) { + return nullptr; + } + + jbyteArray j_array = env->NewByteArray(proto_bytes.size()); + if (j_array == nullptr) { + return nullptr; + } + env->SetByteArrayRegion(j_array, 0, proto_bytes.size(), + reinterpret_cast(proto_bytes.data())); + + return j_array; +} + } // namespace extern "C" { @@ -353,47 +536,12 @@ Java_com_google_libmotionphoto_motionphoto_MetadataEngineJni_parseMetadata( if (!file_path_chars.ok()) { return nullptr; } - std::string file_path(file_path_chars.c_str()); - - namespace mp = libmotionphoto::motion_photo; - namespace ii = libmotionphoto::image_io; - - ii::MessageHandler message_handler; - std::shared_ptr data_segment = - ii::ReadEntireFile(file_path, &message_handler); - if (!data_segment) { - return nullptr; - } - size_t file_size = data_segment->GetLength(); - auto data_source = - std::make_unique(data_segment); - - mp::MetadataEngine engine(&message_handler); - engine.RegisterProvider(std::make_unique()); - - mp::FileType file_type = mp::FileType::kJpeg; - mp::MetadataCollection collection = - engine.Parse(data_source.get(), file_size, file_type); - - for (int i = 0; i < collection.blocks_size(); ++i) { - if (collection.blocks(i).format_identifier() == "container.trailer") { - collection.mutable_blocks(i)->clear_raw_bytes(); - } - } - - std::vector proto_bytes(collection.ByteSizeLong()); - if (!collection.SerializeToArray(proto_bytes.data(), proto_bytes.size())) { - return nullptr; - } - - jbyteArray j_array = env->NewByteArray(proto_bytes.size()); - if (j_array == nullptr) { + int fd = open(file_path_chars.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { return nullptr; } - env->SetByteArrayRegion(j_array, 0, proto_bytes.size(), - reinterpret_cast(proto_bytes.data())); - - return j_array; + return ParseMetadataFromFd(env, fd, /*offset=*/0, /*length=*/0, + /*owns_fd=*/true); } catch (const std::bad_alloc& e) { ThrowOutOfMemoryError(env, e.what()); return nullptr; @@ -414,73 +562,7 @@ Java_com_google_libmotionphoto_motionphoto_MetadataEngineJni_parseMetadataFd__IJ return nullptr; } try { - size_t file_size = 0; - if (jlength > 0) { - file_size = static_cast(jlength); - } else { - struct stat sb; - if (fstat(fd, &sb) != -1 && sb.st_size > 0) { - file_size = static_cast(sb.st_size); - } - } - if (file_size == 0) { - return nullptr; - } - - namespace mp = libmotionphoto::motion_photo; - namespace ii = libmotionphoto::image_io; - - auto heap_buffer = std::make_unique(file_size); - size_t bytes_read = 0; - off_t read_offset = static_cast(joffset); - - while (bytes_read < file_size) { - ssize_t r = pread(fd, heap_buffer.get() + bytes_read, - file_size - bytes_read, read_offset + bytes_read); - if (r <= 0) { - if (r < 0 && errno == EINTR) continue; - break; - } - bytes_read += r; - } - - if (bytes_read < file_size) { - return nullptr; - } - - ii::MessageHandler message_handler; - ii::DataRange range(0, file_size); - std::shared_ptr data_segment = ii::DataSegment::Create( - range, heap_buffer.release(), ii::DataSegment::kDelete); - auto data_source = - std::make_unique(data_segment); - - mp::MetadataEngine engine(&message_handler); - engine.RegisterProvider(std::make_unique()); - - mp::FileType file_type = mp::FileType::kJpeg; - mp::MetadataCollection collection = - engine.Parse(data_source.get(), file_size, file_type); - - for (int i = 0; i < collection.blocks_size(); ++i) { - if (collection.blocks(i).format_identifier() == "container.trailer") { - collection.mutable_blocks(i)->clear_raw_bytes(); - } - } - - std::vector proto_bytes(collection.ByteSizeLong()); - if (!collection.SerializeToArray(proto_bytes.data(), proto_bytes.size())) { - return nullptr; - } - - jbyteArray j_array = env->NewByteArray(proto_bytes.size()); - if (j_array == nullptr) { - return nullptr; - } - env->SetByteArrayRegion(j_array, 0, proto_bytes.size(), - reinterpret_cast(proto_bytes.data())); - - return j_array; + return ParseMetadataFromFd(env, fd, joffset, jlength, /*owns_fd=*/false); } catch (const std::bad_alloc& e) { ThrowOutOfMemoryError(env, e.what()); return nullptr; diff --git a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc index 257eacb..c3588d1 100644 --- a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc +++ b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc @@ -134,7 +134,8 @@ bool LegacyMicroVideoProvider::GetVideoInfo(const RawMetadataBlock& block, *out_offset_in_block = 0; } if (out_length) { - *out_length = block.bytes.size(); + *out_length = (block.total_payload_size > 0) ? block.total_payload_size + : block.bytes.size(); } return true; } From fdaab35ecac9de2fcc286d7504ce55e7abe86134 Mon Sep 17 00:00:00 2001 From: Dichen Zhang <140119224+DichenZhang1@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:24:53 +0000 Subject: [PATCH 3/3] [libmotionphoto] Move legacy MicroVideoOffset to 3P plugin and expose HandlerOptions in ParseMotionPhoto API (#5) Syncs the review updates from cl/991201939 onto `Development`: 1. **Standard Motion Photo XMP parsing & Legacy MicroVideo 3P Plugin separation**: - Restricts `JpegContainerSplitter::ParseVideoLengthFromXmp` to standard Motion Photo metadata (`(GCamera|Camera):MotionPhoto` and `Container:Directory` `Item:Length` where `Item:Semantic="MotionPhoto"` in both attribute and element forms). - Moves legacy `MicroVideoOffset` parsing into `LegacyMicroVideoProvider` (`DecodeToSemantic` -> `LegacyMicroVideoMetadata::micro_video_offset` and `GetVideoInfo`). - Uses `total_payload_size` in `LegacyMicroVideoProvider` so full video payloads can be streamed directly from the underlying data source without buffering the video trailer in `RawMetadataBlock::bytes`. 2. **Expose `HandlerOptions` in `libmotionphoto_api.h` and govern `is_motion_photo` by enabled 3P plugins**: - Exposes `HandlerOptions` (`disable_3p_plugins`, `enabled_3p_plugins`) on `ParseMotionPhotoFrom{File,Fd,Memory}`. - Governs `out_metadata->is_motion_photo` strictly via `MetadataEngine::IsMotionPhoto(collection, engine_options)` so `is_motion_photo` returns `false` when 3P plugins are disabled or only non-matching 3P plugins are enabled, and `true` when all 3P plugins or a matching 3P plugin is enabled. - Adds unit tests in `legacy_micro_video_provider_test.cc` and `libmotionphoto_api_test.cc` covering all 4 `HandlerOptions` states. --- .github/workflows/build_and_test.yml | 4 +- .../motion_photo/jpeg_container_splitter.cc | 149 +++++++++++------- .../src/motion_photo/libmotionphoto_api.cc | 36 +++-- .../tests/libmotionphoto_api_test.cc | 71 +++++++++ .../legacy_micro_video_metadata.proto | 1 + .../legacy_micro_video_provider.cc | 30 +++- .../legacy_micro_video_provider_test.cc | 57 ++++++- libmotionphoto_api.h | 15 +- 8 files changed, 281 insertions(+), 82 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 233bdd9..eec0f8d 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -16,9 +16,9 @@ name: Build and Test (Linux & macOS) on: push: - branches: [ main, master ] + branches: [ main, master, Development ] pull_request: - branches: [ main, master ] + branches: [ main, master, Development ] permissions: contents: read diff --git a/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc b/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc index 6f85936..9fed8ae 100644 --- a/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc +++ b/lib/core/motion_photo/src/motion_photo/jpeg_container_splitter.cc @@ -135,7 +135,7 @@ class JpegContainerSplitter : public ContainerSplitter, ExtractBlock(segment, "XMP", "standard.xmp", 29); // skip namespace if (!blocks_.empty() && blocks_.back().format_identifier == "standard.xmp") { - std::string_view xml( + absl::string_view xml( reinterpret_cast(blocks_.back().bytes.data()), blocks_.back().bytes.size()); size_t length = ParseVideoLengthFromXmp(xml); @@ -164,35 +164,68 @@ class JpegContainerSplitter : public ContainerSplitter, void Finish(image_io::JpegScanner* scanner) override {} private: - static size_t ParseVideoLengthFromXmp(std::string_view xml) { - // 1. Check Container:Directory Item:Length where - // Item:Semantic="MotionPhoto" Handles both Item:Semantic="MotionPhoto" - // Item:Length="..." and reversed attribute order. + static bool HasMotionPhotoFlag(absl::string_view xml) { + if (absl::StrContains(xml, "MotionPhoto=\"1\"") || + absl::StrContains(xml, "MotionPhoto='1'")) { + return true; + } + size_t elem_pos = xml.find(":MotionPhoto>"); + while (elem_pos != absl::string_view::npos) { + size_t val_start = elem_pos + 13; + while (val_start < xml.size() && + (xml[val_start] == ' ' || xml[val_start] == '\t' || + xml[val_start] == '\n' || xml[val_start] == '\r')) { + ++val_start; + } + if (val_start < xml.size() && xml[val_start] == '1') { + size_t val_end = val_start + 1; + while (val_end < xml.size() && + (xml[val_end] == ' ' || xml[val_end] == '\t' || + xml[val_end] == '\n' || xml[val_end] == '\r')) { + ++val_end; + } + if (val_end < xml.size() && xml[val_end] == '<') { + return true; + } + } + elem_pos = xml.find(":MotionPhoto>", elem_pos + 1); + } + return false; + } + + static size_t ParseVideoLengthFromXmp(absl::string_view xml) { + if (!HasMotionPhotoFlag(xml)) { + return 0; + } + + // 1. Check Container:Directory Item:Length in attribute form where + // Item:Semantic="MotionPhoto" (handles any attribute order and quote + // style). size_t motion_photo_pos = xml.find("MotionPhoto"); - while (motion_photo_pos != std::string_view::npos) { + while (motion_photo_pos != absl::string_view::npos) { // Find enclosing tag bounds '<...>' size_t tag_start = xml.rfind('<', motion_photo_pos); size_t tag_end = xml.find('>', motion_photo_pos); - if (tag_start != std::string_view::npos && - tag_end != std::string_view::npos && tag_start < tag_end) { - std::string_view tag = xml.substr(tag_start, tag_end - tag_start + 1); - absl::string_view absl_tag(tag.data(), tag.size()); - if (absl::StrContains(absl_tag, "Item:Length") || - absl::StrContains(absl_tag, "Length")) { - size_t len_attr = tag.find("Length=\""); - size_t val_start = (len_attr != std::string_view::npos) - ? len_attr + 8 - : std::string_view::npos; - if (val_start == std::string_view::npos) { - len_attr = tag.find("Length='"); - if (len_attr != std::string_view::npos) { - val_start = len_attr + 8; + if (tag_start != absl::string_view::npos && + tag_end != absl::string_view::npos && tag_start < tag_end) { + absl::string_view tag = xml.substr(tag_start, tag_end - tag_start + 1); + if ((absl::StrContains(tag, "Item:Semantic=\"MotionPhoto\"") || + absl::StrContains(tag, "Item:Semantic='MotionPhoto'")) && + absl::StrContains(tag, "Item:Length")) { + size_t len_attr = tag.find("Item:Length=\""); + size_t val_start = (len_attr != absl::string_view::npos) + ? len_attr + 13 + : absl::string_view::npos; + if (val_start == absl::string_view::npos) { + len_attr = tag.find("Item:Length='"); + if (len_attr != absl::string_view::npos) { + val_start = len_attr + 13; } } - if (val_start != std::string_view::npos) { + if (val_start != absl::string_view::npos) { char quote = tag[val_start - 1]; size_t val_end = tag.find(quote, val_start); - if (val_end != std::string_view::npos) { + if (val_end != absl::string_view::npos) { std::string val_str(tag.substr(val_start, val_end - val_start)); try { int64_t len = std::stoll(val_str); @@ -206,50 +239,52 @@ class JpegContainerSplitter : public ContainerSplitter, motion_photo_pos = xml.find("MotionPhoto", motion_photo_pos + 1); } - // 2. Check GCamera:MicroVideoOffset / Camera:MicroVideoOffset - size_t offset_pos = xml.find("MicroVideoOffset=\""); - size_t val_start = (offset_pos != std::string_view::npos) - ? offset_pos + 18 - : std::string_view::npos; - if (val_start == std::string_view::npos) { - offset_pos = xml.find("MicroVideoOffset='"); - if (offset_pos != std::string_view::npos) { - val_start = offset_pos + 18; + // 2. Check Container:Directory Item:Length in XML element form: + // MotionPhoto ... + // ... + size_t semantic_elem = xml.find(""); + while (semantic_elem != absl::string_view::npos) { + size_t semantic_end = xml.find("", semantic_elem); + if (semantic_end == absl::string_view::npos) { + break; } - } - if (val_start != std::string_view::npos) { - char quote = xml[val_start - 1]; - size_t val_end = xml.find(quote, val_start); - if (val_end != std::string_view::npos) { - std::string val_str(xml.substr(val_start, val_end - val_start)); - try { - int64_t len = std::stoll(val_str); - if (len > 0) return static_cast(len); - } catch (...) { + absl::string_view semantic_val = + xml.substr(semantic_elem + 15, semantic_end - (semantic_elem + 15)); + if (absl::StrContains(semantic_val, "MotionPhoto")) { + size_t item_start = xml.rfind("... - size_t elem_pos = xml.find(":MicroVideoOffset>"); - if (elem_pos != std::string_view::npos) { - size_t text_start = elem_pos + 18; - size_t text_end = xml.find('<', text_start); - if (text_end != std::string_view::npos) { - std::string val_str(xml.substr(text_start, text_end - text_start)); - try { - int64_t len = std::stoll(val_str); - if (len > 0) return static_cast(len); - } catch (...) { + size_t item_end = xml.find("", semantic_end); + if (item_end == absl::string_view::npos) { + item_end = xml.find("", semantic_end); + } + if (item_start != absl::string_view::npos && + item_end != absl::string_view::npos && item_start < item_end) { + absl::string_view item_block = + xml.substr(item_start, item_end - item_start); + size_t len_start = item_block.find(""); + size_t len_end = item_block.find(""); + if (len_start != absl::string_view::npos && + len_end != absl::string_view::npos && len_start + 13 < len_end) { + std::string val_str( + item_block.substr(len_start + 13, len_end - (len_start + 13))); + try { + int64_t len = std::stoll(val_str); + if (len > 0) return static_cast(len); + } catch (...) { + } + } } } + semantic_elem = xml.find("", semantic_end + 16); } return 0; } - void ExtractBlock(const image_io::JpegSegment& segment, std::string_view type, - std::string_view format_id, size_t skip_bytes) { + void ExtractBlock(const image_io::JpegSegment& segment, absl::string_view type, + absl::string_view format_id, size_t skip_bytes) { size_t start = segment.GetPayloadDataLocation() + skip_bytes; size_t end = segment.GetDataRange().GetEnd(); if (start < end) { diff --git a/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc b/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc index 87a8bbd..e514b12 100644 --- a/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc +++ b/lib/core/motion_photo/src/motion_photo/libmotionphoto_api.cc @@ -67,10 +67,10 @@ bool WriteBufferToFile(const std::string& path, const uint8_t* data, size_t size } // Internal helper copying nested image_io fields into clean native structs. -void PopulateNativeMetadata(const ::libmotionphoto::motion_photo::MotionPhoto& photo, - MotionPhotoMetadata* out) { +void PopulateNativeMetadata( + const ::libmotionphoto::motion_photo::MotionPhoto& photo, + MotionPhotoMetadata* out) { if (out == nullptr) return; - out->is_motion_photo = photo.IsMotionPhoto(); const auto& camera = photo.GetCameraMetadata(); if (camera.motion_photo_presentation_timestamp_us.WasAssigned() && camera.motion_photo_presentation_timestamp_us.IsValid()) { @@ -233,7 +233,8 @@ class FdDataSource : public ::photos_editing_formats::image_io::DataSource { bool ParseMotionPhotoFromFile(const std::string& filepath, MotionPhotoMetadata* out_metadata, - MessageCallback callback) { + MessageCallback callback, + const HandlerOptions& options) { ::photos_editing_formats::image_io::MessageHandler message_handler; if (callback) { message_handler.SetMessageWriter( @@ -263,7 +264,10 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, ::libmotionphoto::motion_photo::MetadataCollection collection = engine.Parse(&data_source, file_size, file_type); - bool is_mp = engine.IsMotionPhoto(collection); + ::libmotionphoto::motion_photo::HandlerOptions engine_options; + engine_options.disable_3p_plugins = options.disable_3p_plugins; + engine_options.enabled_3p_plugins = options.enabled_3p_plugins; + bool is_mp = engine.IsMotionPhoto(collection, engine_options); if (out_metadata != nullptr) { out_metadata->is_motion_photo = is_mp; @@ -273,7 +277,7 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, size_t bytes_parsed = 0; for (const auto& block : collection.blocks()) { - if (block.has_video_length() && block.video_length() > 0) { + if (is_mp && block.has_video_length() && block.video_length() > 0) { out_metadata->video_length = block.video_length(); } if (block.type() == ::libmotionphoto::motion_photo::BLOCK_TYPE_XMP || @@ -332,7 +336,8 @@ bool ParseMotionPhotoFromFile(const std::string& filepath, bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, MotionPhotoMetadata* out_metadata, - MessageCallback callback) { + MessageCallback callback, + const HandlerOptions& options) { if (fd < 0 || offset < 0 || length <= 0) { return false; } @@ -375,7 +380,10 @@ bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, ::libmotionphoto::motion_photo::MetadataCollection collection = engine.Parse(&data_source, length, file_type); - bool is_mp = engine.IsMotionPhoto(collection); + ::libmotionphoto::motion_photo::HandlerOptions engine_options; + engine_options.disable_3p_plugins = options.disable_3p_plugins; + engine_options.enabled_3p_plugins = options.enabled_3p_plugins; + bool is_mp = engine.IsMotionPhoto(collection, engine_options); if (out_metadata != nullptr) { out_metadata->is_motion_photo = is_mp; @@ -385,7 +393,7 @@ bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, size_t bytes_parsed = 0; for (const auto& block : collection.blocks()) { - if (block.has_video_length() && block.video_length() > 0) { + if (is_mp && block.has_video_length() && block.video_length() > 0) { out_metadata->video_length = block.video_length(); } if (block.type() == ::libmotionphoto::motion_photo::BLOCK_TYPE_XMP || @@ -444,7 +452,8 @@ bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, bool ParseMotionPhotoFromMemory(const uint8_t* data, size_t size, MotionPhotoMetadata* out_metadata, - MessageCallback callback) { + MessageCallback callback, + const HandlerOptions& options) { if (data == nullptr || size == 0) { return false; } @@ -476,7 +485,10 @@ bool ParseMotionPhotoFromMemory(const uint8_t* data, size_t size, ::libmotionphoto::motion_photo::MetadataCollection collection = engine.Parse(&data_source, size, file_type); - bool is_mp = engine.IsMotionPhoto(collection); + ::libmotionphoto::motion_photo::HandlerOptions engine_options; + engine_options.disable_3p_plugins = options.disable_3p_plugins; + engine_options.enabled_3p_plugins = options.enabled_3p_plugins; + bool is_mp = engine.IsMotionPhoto(collection, engine_options); if (out_metadata != nullptr) { out_metadata->is_motion_photo = is_mp; @@ -486,7 +498,7 @@ bool ParseMotionPhotoFromMemory(const uint8_t* data, size_t size, size_t bytes_parsed = 0; for (const auto& block : collection.blocks()) { - if (block.has_video_length() && block.video_length() > 0) { + if (is_mp && block.has_video_length() && block.video_length() > 0) { out_metadata->video_length = block.video_length(); } if (block.type() == ::libmotionphoto::motion_photo::BLOCK_TYPE_XMP || diff --git a/lib/core/motion_photo/tests/libmotionphoto_api_test.cc b/lib/core/motion_photo/tests/libmotionphoto_api_test.cc index 3ca1267..f312bd2 100644 --- a/lib/core/motion_photo/tests/libmotionphoto_api_test.cc +++ b/lib/core/motion_photo/tests/libmotionphoto_api_test.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -300,5 +301,75 @@ TEST(LibMotionPhotoApiTest, BitReaderAndBoxReaderBoundarySafe) { &metadata)); } +TEST(LibMotionPhotoApiTest, ParseMotionPhoto3pPluginOptions) { + // Construct synthetic Legacy MicroVideo JPEG in memory (recognized by the + // "legacy_micro_video" 3P plugin, not by 1P GoogleMotionPhotoProvider). + std::string xmp_content = + ""; + + std::string jpeg_data; + jpeg_data.push_back(static_cast(0xFF)); + jpeg_data.push_back(static_cast(0xD8)); // SOI + jpeg_data.push_back(static_cast(0xFF)); + jpeg_data.push_back(static_cast(0xE1)); // APP1 + uint16_t app1_len = 2 + 29 + xmp_content.size(); + jpeg_data.push_back(static_cast(app1_len >> 8)); + jpeg_data.push_back(static_cast(app1_len & 0xFF)); + jpeg_data.append("http://ns.adobe.com/xap/1.0/\0", 29); + jpeg_data.append(xmp_content); + jpeg_data.push_back(static_cast(0xFF)); + jpeg_data.push_back(static_cast(0xD9)); // EOI + std::string mp4_trailer = "....ftypmp42...."; + jpeg_data.append(mp4_trailer); + + const auto* raw_ptr = reinterpret_cast(jpeg_data.data()); + size_t raw_size = jpeg_data.size(); + + // 1. Default (all 3P plugins enabled): is_motion_photo should be true. + MotionPhotoMetadata meta_default; + EXPECT_TRUE(ParseMotionPhotoFromMemory(raw_ptr, raw_size, &meta_default)); + EXPECT_TRUE(meta_default.is_motion_photo); + EXPECT_EQ(meta_default.video_length, + static_cast(mp4_trailer.size())); + + // 2. 3P plugins disabled: is_motion_photo should be false. + HandlerOptions disabled_opts; + disabled_opts.disable_3p_plugins = true; + MotionPhotoMetadata meta_disabled; + EXPECT_FALSE(ParseMotionPhotoFromMemory(raw_ptr, raw_size, &meta_disabled, + nullptr, disabled_opts)); + EXPECT_FALSE(meta_disabled.is_motion_photo); + EXPECT_EQ(meta_disabled.video_length, 0); + + // 3. Specific 3P plugin enabled ("legacy_micro_video") and recognizes format: + // is_motion_photo should be true. + HandlerOptions enabled_matching_opts; + enabled_matching_opts.disable_3p_plugins = false; + enabled_matching_opts.enabled_3p_plugins = {"legacy_micro_video"}; + MotionPhotoMetadata meta_matching; + EXPECT_TRUE(ParseMotionPhotoFromMemory(raw_ptr, raw_size, &meta_matching, + nullptr, enabled_matching_opts)); + EXPECT_TRUE(meta_matching.is_motion_photo); + EXPECT_EQ(meta_matching.video_length, + static_cast(mp4_trailer.size())); + + // 4. Specific 3P plugin enabled ("other_plugin") that does not recognize + // format: is_motion_photo should be false. + HandlerOptions enabled_unmatched_opts; + enabled_unmatched_opts.disable_3p_plugins = false; + enabled_unmatched_opts.enabled_3p_plugins = {"other_plugin"}; + MotionPhotoMetadata meta_unmatched; + EXPECT_FALSE(ParseMotionPhotoFromMemory(raw_ptr, raw_size, &meta_unmatched, + nullptr, enabled_unmatched_opts)); + EXPECT_FALSE(meta_unmatched.is_motion_photo); + EXPECT_EQ(meta_unmatched.video_length, 0); +} + } // namespace api } // namespace libmotionphoto diff --git a/lib/core/plugins/legacy_micro_video/legacy_micro_video_metadata.proto b/lib/core/plugins/legacy_micro_video/legacy_micro_video_metadata.proto index 23f9632..af1c057 100644 --- a/lib/core/plugins/legacy_micro_video/legacy_micro_video_metadata.proto +++ b/lib/core/plugins/legacy_micro_video/legacy_micro_video_metadata.proto @@ -24,4 +24,5 @@ message LegacyMicroVideoMetadata { int32 legacy_micro_video = 1; int32 version = 2; int64 primary_image_timestamp_us = 3; + int64 micro_video_offset = 4; } diff --git a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc index c3588d1..72d4eb0 100644 --- a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc +++ b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider.cc @@ -23,7 +23,7 @@ #include #include -#include "legacy_micro_video_metadata.pb.h" +#include "legacy_micro_video_metadata.pb.h" // NOLINT #include "motion_photo/metadata_block.h" namespace libmotionphoto { @@ -31,7 +31,7 @@ namespace motion_photo { namespace { -int64_t ExtractInt64Attr(std::string_view xml, const std::string& attr_name) { +int64_t ExtractInt64Attr(absl::string_view xml, const std::string& attr_name) { std::regex attr_regex(R"((?:GCamera|Camera):)" + attr_name + R"(\s*=\s*["'](-?\d+)["'])"); std::cmatch match; @@ -54,12 +54,12 @@ int64_t ExtractInt64Attr(std::string_view xml, const std::string& attr_name) { bool LegacyMicroVideoProvider::Identify(const RawMetadataBlock& block) { if (block.type == "XMP" && block.format_identifier == "standard.xmp") { absl::string_view xml(reinterpret_cast(block.bytes.data()), - block.bytes.size()); + block.bytes.size()); return absl::StrContains(xml, "MicroVideo"); } if (block.format_identifier == "container.trailer") { if (block.bytes.size() >= 8) { - std::string_view tag( + absl::string_view tag( reinterpret_cast(block.bytes.data() + 4), 4); if (tag == "ftyp" || tag == "moov" || tag == "mdat" || tag == "free" || tag == "skip" || tag == "wide") { @@ -76,7 +76,7 @@ bool LegacyMicroVideoProvider::DecodeToSemantic( if (block.type != "XMP") { return false; } - std::string_view xml(reinterpret_cast(block.bytes.data()), + absl::string_view xml(reinterpret_cast(block.bytes.data()), block.bytes.size()); LegacyMicroVideoMetadata metadata; @@ -96,6 +96,11 @@ bool LegacyMicroVideoProvider::DecodeToSemantic( metadata.set_version(version); } + int64_t offset = ExtractInt64Attr(xml, "MicroVideoOffset"); + if (offset > 0) { + metadata.set_micro_video_offset(offset); + } + if (out_type_url) { *out_type_url = "type.googleapis.com/libmotionphoto.motion_photo." @@ -129,6 +134,21 @@ bool LegacyMicroVideoProvider::IsMotionPhoto( bool LegacyMicroVideoProvider::GetVideoInfo(const RawMetadataBlock& block, size_t* out_offset_in_block, size_t* out_length) const { + if (block.type == "XMP" && block.format_identifier == "standard.xmp") { + absl::string_view xml(reinterpret_cast(block.bytes.data()), + block.bytes.size()); + int64_t offset = ExtractInt64Attr(xml, "MicroVideoOffset"); + if (offset > 0) { + if (out_offset_in_block) { + *out_offset_in_block = 0; + } + if (out_length) { + *out_length = static_cast(offset); + } + return true; + } + return false; + } if (block.format_identifier == "container.trailer") { if (out_offset_in_block) { *out_offset_in_block = 0; diff --git a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider_test.cc b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider_test.cc index 9fc9fe6..d306eec 100644 --- a/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider_test.cc +++ b/lib/core/plugins/legacy_micro_video/legacy_micro_video_provider_test.cc @@ -26,7 +26,7 @@ #include "image_io/base/message_handler.h" #include "image_io/base/string_ref_data_source.h" -#include "legacy_micro_video_metadata.pb.h" +#include "legacy_micro_video_metadata.pb.h" // NOLINT #include "motion_photo/google_motion_photo_provider.h" #include "motion_photo/metadata_block.h" #include "motion_photo/metadata_engine.h" @@ -126,6 +126,7 @@ TEST(LegacyMicroVideoProviderTest, DecodeToSemantic) { EXPECT_EQ(metadata.legacy_micro_video(), 1); EXPECT_EQ(metadata.primary_image_timestamp_us(), 1475413); EXPECT_EQ(metadata.version(), 1); + EXPECT_EQ(metadata.micro_video_offset(), 4261545); EXPECT_TRUE(provider.IsMotionPhoto(type_url, payload_str)); } @@ -133,13 +134,30 @@ TEST(LegacyMicroVideoProviderTest, DecodeToSemantic) { TEST(LegacyMicroVideoProviderTest, GetVideoInfo) { LegacyMicroVideoProvider provider; + RawMetadataBlock xmp_block; + xmp_block.type = "XMP"; + xmp_block.format_identifier = "standard.xmp"; + std::string xmp_data = + ""; + xmp_block.bytes.assign(xmp_data.begin(), xmp_data.end()); + + size_t offset_in_block = 0; + size_t length = 0; + EXPECT_TRUE(provider.GetVideoInfo(xmp_block, &offset_in_block, &length)); + EXPECT_EQ(offset_in_block, 0); + EXPECT_EQ(length, 4261545); + RawMetadataBlock trailer_block; trailer_block.type = "PROPRIETARY"; trailer_block.format_identifier = "container.trailer"; trailer_block.bytes.resize(4261545); - size_t offset_in_block = 0; - size_t length = 0; + offset_in_block = 0; + length = 0; EXPECT_TRUE(provider.GetVideoInfo(trailer_block, &offset_in_block, &length)); EXPECT_EQ(offset_in_block, 0); EXPECT_EQ(length, 4261545); @@ -231,6 +249,39 @@ TEST(LegacyMicroVideoProviderTest, GlobalRegistryIntegration) { EXPECT_TRUE(MetadataEngine::IsMotionPhoto(collection)); EXPECT_EQ(collection.blocks_size(), 2); + + // 1. When 3P plugins are disabled, IsMotionPhoto returns false. + HandlerOptions disabled_options; + disabled_options.disable_3p_plugins = true; + EXPECT_FALSE(MetadataEngine::IsMotionPhoto(collection, disabled_options)); + + // Even if enabled_3p_plugins lists the plugin, disable_3p_plugins=true takes + // precedence and returns false. + disabled_options.enabled_3p_plugins = {"legacy_micro_video"}; + EXPECT_FALSE(MetadataEngine::IsMotionPhoto(collection, disabled_options)); + + // 2. When all 3P plugins are enabled (disable_3p_plugins=false, + // enabled_3p_plugins empty), IsMotionPhoto returns true. + HandlerOptions all_enabled_options; + all_enabled_options.disable_3p_plugins = false; + EXPECT_TRUE(MetadataEngine::IsMotionPhoto(collection, all_enabled_options)); + + // 3. When certain 3P plugins are enabled and the format is recognized by an + // enabled 3P plugin, IsMotionPhoto returns true. + HandlerOptions specific_enabled_options; + specific_enabled_options.disable_3p_plugins = false; + specific_enabled_options.enabled_3p_plugins = {"other_plugin", + "legacy_micro_video"}; + EXPECT_TRUE( + MetadataEngine::IsMotionPhoto(collection, specific_enabled_options)); + + // 4. When certain 3P plugins are enabled and the format is NOT recognized by + // any enabled 3P plugin, IsMotionPhoto returns false. + HandlerOptions unmatched_enabled_options; + unmatched_enabled_options.disable_3p_plugins = false; + unmatched_enabled_options.enabled_3p_plugins = {"other_plugin"}; + EXPECT_FALSE( + MetadataEngine::IsMotionPhoto(collection, unmatched_enabled_options)); } TEST(LegacyMicroVideoProviderTest, ExtractMetadata_LegacyMicroVideo) { diff --git a/libmotionphoto_api.h b/libmotionphoto_api.h index 0d7089b..3406e2d 100644 --- a/libmotionphoto_api.h +++ b/libmotionphoto_api.h @@ -74,6 +74,12 @@ struct MotionPhotoMetadata { ContainerMetadata container; }; +// Options for controlling third-party (3P) metadata plugin evaluation. +struct HandlerOptions { + bool disable_3p_plugins = false; + std::vector enabled_3p_plugins; +}; + // ========================================================================= // 1. Parsing & Inspection (Decoding / Metadata Extraction) // ========================================================================= @@ -82,19 +88,22 @@ struct MotionPhotoMetadata { // Returns true if the file is a valid Motion Photo. bool ParseMotionPhotoFromFile(const std::string& filepath, MotionPhotoMetadata* out_metadata, - MessageCallback callback = nullptr); + MessageCallback callback = nullptr, + const HandlerOptions& options = {}); // Parse Motion Photo metadata from a POSIX file descriptor slice (fd, offset, length). // Returns true if parsing succeeded and the payload is a valid Motion Photo. bool ParseMotionPhotoFromFd(int fd, int64_t offset, int64_t length, MotionPhotoMetadata* out_metadata, - MessageCallback callback = nullptr); + MessageCallback callback = nullptr, + const HandlerOptions& options = {}); // Parse Motion Photo metadata from an in-memory byte buffer. // Returns true if parsing succeeded and the payload is a valid Motion Photo. bool ParseMotionPhotoFromMemory(const uint8_t* data, size_t size, MotionPhotoMetadata* out_metadata, - MessageCallback callback = nullptr); + MessageCallback callback = nullptr, + const HandlerOptions& options = {}); // ========================================================================= // 2. Container Validation (Checking)