From 0044976e9abade73ef6c37167f1055051aec10ab Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Mon, 16 Mar 2026 16:56:51 +0200 Subject: [PATCH 1/3] Added AppleFramework Photo library Added PHAsset handling to retrieve gps data --- app/CMakeLists.txt | 1 + app/ios/iosinterface.mm | 39 ++++++++++++++++++++++++++++----- cmake/FindAppleFrameworks.cmake | 1 + 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e474aa9b2..3f3a8daae 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -574,6 +574,7 @@ endif () if (IOS) target_link_libraries( MerginMaps PUBLIC AppleFrameworks::CoreLocation AppleFrameworks::CoreHaptics + AppleFrameworks::Photos ) # TODO is this needed? this change requires cmake 3.28+ # qt_add_ios_ffmpeg_libraries(MerginMaps) # Qt Multimedia diff --git a/app/ios/iosinterface.mm b/app/ios/iosinterface.mm index 7a8e3cbf1..e5e1c47ab 100644 --- a/app/ios/iosinterface.mm +++ b/app/ios/iosinterface.mm @@ -28,6 +28,7 @@ #import #import #import +#import @implementation IOSInterface @@ -229,13 +230,41 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler else { // Gallery handling - // Copy an image with metadata from imageURL to targetPath - NSURL *infoImageUrl = info[UIImagePickerControllerImageURL]; - if ( !InputUtils::copyFile( QString::fromNSString( infoImageUrl.absoluteString ), QString::fromNSString( imagePath ) ) ) + // use PHAsset to request the original image data with full EXIF (including GPS). + PHAsset *asset = info[UIImagePickerControllerPHAsset]; + if ( asset ) { - err = QStringLiteral( "Copying image from a gallery failed." ); + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.version = PHImageRequestOptionsVersionOriginal; + options.networkAccessAllowed = YES; + + __block BOOL writeSuccess = NO; + [[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset + options:options + resultHandler: ^ ( NSData * imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary * requestInfo ) + { + if ( imageData ) + { + writeSuccess = [imageData writeToFile:imagePath atomically:YES]; + } + }]; + + if ( !writeSuccess ) + { + err = QStringLiteral( "Copying image from gallery failed." ); + } + } + else + { + // fallback: copy file directly, even though GPS metadata might be deleted by iOS + NSURL *infoImageUrl = info[UIImagePickerControllerImageURL]; + if ( !InputUtils::copyFile( QString::fromNSString( infoImageUrl.absoluteString ), QString::fromNSString( imagePath ) ) ) + { + err = QStringLiteral( "Copying image from a gallery failed." ); + } } - infoImageUrl = nil; } [picker dismissViewControllerAnimated:YES completion:nil]; diff --git a/cmake/FindAppleFrameworks.cmake b/cmake/FindAppleFrameworks.cmake index 53b916b9c..bf4abd51c 100644 --- a/cmake/FindAppleFrameworks.cmake +++ b/cmake/FindAppleFrameworks.cmake @@ -19,6 +19,7 @@ set(APPLE_FRAMEWORKS SystemConfiguration CoreLocation CoreHaptics + Photos ) foreach (framework ${APPLE_FRAMEWORKS}) From a0d3e4a31a111cd9a2d9ee76e64a9ce20cfcf419 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Tue, 17 Mar 2026 11:58:05 +0200 Subject: [PATCH 2/3] Implemented interface for gallery picker Got rid of deprecated API and used PHPickerViewController --- app/CMakeLists.txt | 2 +- app/ios/iosimagepicker.mm | 1 - app/ios/iosinterface.mm | 80 +++++++++++---------------------- app/ios/iosviewdelegate.h | 8 ++++ app/ios/iosviewdelegate.mm | 65 +++++++++++++++++++++++++-- cmake/FindAppleFrameworks.cmake | 2 +- 6 files changed, 96 insertions(+), 62 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3f3a8daae..0c0e86e58 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -574,7 +574,7 @@ endif () if (IOS) target_link_libraries( MerginMaps PUBLIC AppleFrameworks::CoreLocation AppleFrameworks::CoreHaptics - AppleFrameworks::Photos + AppleFrameworks::PhotosUI ) # TODO is this needed? this change requires cmake 3.28+ # qt_add_ios_ffmpeg_libraries(MerginMaps) # Qt Multimedia diff --git a/app/ios/iosimagepicker.mm b/app/ios/iosimagepicker.mm index 37e7217cf..3baa6e060 100644 --- a/app/ios/iosimagepicker.mm +++ b/app/ios/iosimagepicker.mm @@ -16,7 +16,6 @@ #include #include #include -#include #include #include "iosinterface.h" diff --git a/app/ios/iosinterface.mm b/app/ios/iosinterface.mm index e5e1c47ab..e7d412e12 100644 --- a/app/ios/iosinterface.mm +++ b/app/ios/iosinterface.mm @@ -18,7 +18,6 @@ #import #import "ios/iosinterface.h" #include "iosviewdelegate.h" -#include "inpututils.h" #import #include "position/positionkit.h" #include "compass.h" @@ -28,7 +27,6 @@ #import #import #import -#import @implementation IOSInterface @@ -178,7 +176,26 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler UIWindow *rootWindow = app.windows[0]; UIViewController *rootViewController = rootWindow.rootViewController; - if ( ![UIImagePickerController isSourceTypeAvailable:( UIImagePickerControllerSourceType ) sourceType] ) + bool isCamera = ( UIImagePickerControllerSourceType ) sourceType == UIImagePickerControllerSourceTypeCamera; + + if ( !isCamera ) + { + // Gallery: use PHPickerViewController + PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init]; + config.filter = [PHPickerFilter imagesFilter]; + config.selectionLimit = 1; + + PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:config]; + static IOSGalleryPickerDelegate *galleryDelegate = nullptr; + galleryDelegate = [[IOSGalleryPickerDelegate alloc] initWithHandler:handler]; + picker.delegate = galleryDelegate; + + [rootViewController presentViewController:picker animated:YES completion:nil]; + return; + } + + // Camera: use UIImagePickerController + if ( ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] ) { NSString *alertTitle = @"Image picker"; NSString *alertMessage = @"The functionality is not available"; @@ -189,7 +206,7 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *actionOk = [UIAlertAction actionWithTitle:alertOkButtonText style:UIAlertActionStyleDefault - handler:nil]; //You can use a block here to handle a press on this button + handler:nil]; [alertController addAction:actionOk]; [rootViewController presentViewController:alertController animated:YES completion:nil]; } @@ -197,14 +214,14 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; imagePickerController = picker; - picker.sourceType = ( UIImagePickerControllerSourceType ) sourceType; + picker.sourceType = UIImagePickerControllerSourceTypeCamera; static IOSViewDelegate *delegate = nullptr; delegate = [[IOSViewDelegate alloc] initWithHandler:handler]; [[NSNotificationCenter defaultCenter] addObserverForName:@"_UIImagePickerControllerUserDidCaptureItem" object:nil queue:nil usingBlock: ^ ( NSNotification * _Nonnull notification ) { Q_UNUSED( notification ) - // Fetch GPS data when an image is captured + // Fetch GPS data from positionKit at the moment of capture mGpsData = getGPSData( delegate->handler->positionKit(), delegate->handler->compass() ); }]; @@ -219,60 +236,13 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler delegate->processingPicture = YES; NSString *imagePath = generateImagePath( delegate->handler->targetDir().toNSString() ); - QString err; - - bool isCameraPhoto = picker.sourceType == UIImagePickerControllerSourceType::UIImagePickerControllerSourceTypeCamera; - if ( isCameraPhoto ) - { - // Camera handling - err = [IOSInterface handleCameraPhoto:info:imagePath]; - } - else - { - // Gallery handling - // use PHAsset to request the original image data with full EXIF (including GPS). - PHAsset *asset = info[UIImagePickerControllerPHAsset]; - if ( asset ) - { - PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; - options.synchronous = YES; - options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; - options.version = PHImageRequestOptionsVersionOriginal; - options.networkAccessAllowed = YES; - - __block BOOL writeSuccess = NO; - [[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset - options:options - resultHandler: ^ ( NSData * imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary * requestInfo ) - { - if ( imageData ) - { - writeSuccess = [imageData writeToFile:imagePath atomically:YES]; - } - }]; - - if ( !writeSuccess ) - { - err = QStringLiteral( "Copying image from gallery failed." ); - } - } - else - { - // fallback: copy file directly, even though GPS metadata might be deleted by iOS - NSURL *infoImageUrl = info[UIImagePickerControllerImageURL]; - if ( !InputUtils::copyFile( QString::fromNSString( infoImageUrl.absoluteString ), QString::fromNSString( imagePath ) ) ) - { - err = QStringLiteral( "Copying image from a gallery failed." ); - } - } - } + QString err = [IOSInterface handleCameraPhoto:info:imagePath]; [picker dismissViewControllerAnimated:YES completion:nil]; if ( delegate->handler ) { QVariantMap data; - QString imagePathData( [imagePath UTF8String] ); - data["imagePath"] = imagePathData; + data["imagePath"] = QString( [imagePath UTF8String] ); data["error"] = err; QMetaObject::invokeMethod( delegate->handler, "onImagePickerFinished", Qt::DirectConnection, Q_ARG( bool, err.isEmpty() ), diff --git a/app/ios/iosviewdelegate.h b/app/ios/iosviewdelegate.h index 92e5c4d21..7947f1b63 100644 --- a/app/ios/iosviewdelegate.h +++ b/app/ios/iosviewdelegate.h @@ -17,6 +17,7 @@ #define IOSVIEWDELEGATE_H #include +#import #include "iosimagepicker.h" /** @@ -36,4 +37,11 @@ UINavigationControllerDelegate> - ( id ) initWithHandler:( IOSImagePicker * )handler; @end +/** + * we use PHPickerViewController delegate for gallery image selection, which keeps GPS metadata + */ +@interface IOSGalleryPickerDelegate : NSObject +- ( instancetype ) initWithHandler:( IOSImagePicker * )handler; +@end + #endif // IOSVIEWDELEGATE_H diff --git a/app/ios/iosviewdelegate.mm b/app/ios/iosviewdelegate.mm index df43b62f6..9b2e5573c 100644 --- a/app/ios/iosviewdelegate.mm +++ b/app/ios/iosviewdelegate.mm @@ -14,12 +14,9 @@ ***************************************************************************/ #include +#include #import "iosviewdelegate.h" -@interface IOSViewDelegate() - -@end - @implementation IOSViewDelegate -( id ) initWithHandler:( IOSImagePicker * )handler @@ -50,3 +47,63 @@ - ( void )imagePickerControllerDidCancel:( UIImagePickerController * )picker } @end + +@implementation IOSGalleryPickerDelegate +{ + QPointer _handler; +} + +- ( instancetype ) initWithHandler:( IOSImagePicker * )handler +{ + self = [super init]; + if ( self ) + { + _handler = handler; + } + return self; +} + +- ( void )picker:( PHPickerViewController * )picker didFinishPicking:( NSArray * )results +{ + [picker dismissViewControllerAnimated:YES completion:nil]; + + if ( results.count == 0 ) + { + return; // user cancelled + } + + PHPickerResult *result = results.firstObject; + + NSDateFormatter *df = [[NSDateFormatter alloc] init]; + [df setDateFormat:@"yyyyMMdd_HHmmss"]; + NSString *fileName = [[df stringFromDate:[NSDate date]] stringByAppendingString:@".jpg"]; + NSString *imagePath = [_handler->targetDir().toNSString() stringByAppendingPathComponent:fileName]; + + [result.itemProvider loadDataRepresentationForTypeIdentifier:@"public.jpeg" + completionHandler: ^ ( NSData * data, NSError * error ) + { + BOOL writeSuccess = data && !error && [data writeToFile:imagePath atomically:YES]; + if ( !writeSuccess ) + { + qWarning() << "Gallery Picker: failed to write image data to" << QString::fromNSString( imagePath ); + } + + dispatch_async( dispatch_get_main_queue(), ^ + { + if ( _handler ) + { + QVariantMap resultData; + resultData["imagePath"] = QString::fromNSString( imagePath ); + if ( !writeSuccess ) + { + resultData["error"] = QStringLiteral( "Copying image from gallery failed." ); + } + QMetaObject::invokeMethod( _handler, "onImagePickerFinished", Qt::DirectConnection, + Q_ARG( bool, writeSuccess ), + Q_ARG( const QVariantMap, resultData ) ); + } + } ); + }]; +} + +@end diff --git a/cmake/FindAppleFrameworks.cmake b/cmake/FindAppleFrameworks.cmake index bf4abd51c..17be9bcab 100644 --- a/cmake/FindAppleFrameworks.cmake +++ b/cmake/FindAppleFrameworks.cmake @@ -19,7 +19,7 @@ set(APPLE_FRAMEWORKS SystemConfiguration CoreLocation CoreHaptics - Photos + PhotosUI ) foreach (framework ${APPLE_FRAMEWORKS}) From ed9f03e87b767f4511f3218532aa187555be5250 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Tue, 17 Mar 2026 16:20:36 +0200 Subject: [PATCH 3/3] Small addition --- app/ios/iosinterface.mm | 2 +- app/ios/iosviewdelegate.mm | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/ios/iosinterface.mm b/app/ios/iosinterface.mm index e7d412e12..8b3894cd4 100644 --- a/app/ios/iosinterface.mm +++ b/app/ios/iosinterface.mm @@ -221,7 +221,7 @@ +( void )showImagePicker:( int )sourceType : ( IOSImagePicker * )handler [[NSNotificationCenter defaultCenter] addObserverForName:@"_UIImagePickerControllerUserDidCaptureItem" object:nil queue:nil usingBlock: ^ ( NSNotification * _Nonnull notification ) { Q_UNUSED( notification ) - // Fetch GPS data from positionKit at the moment of capture + // Fetch GPS data when an image is captured mGpsData = getGPSData( delegate->handler->positionKit(), delegate->handler->compass() ); }]; diff --git a/app/ios/iosviewdelegate.mm b/app/ios/iosviewdelegate.mm index 9b2e5573c..362fbb204 100644 --- a/app/ios/iosviewdelegate.mm +++ b/app/ios/iosviewdelegate.mm @@ -72,6 +72,11 @@ - ( void )picker:( PHPickerViewController * )picker didFinishPicking:( NSArray