v0.8.0: enrich XMP metadata
This commit is contained in:
@@ -20,6 +20,8 @@ char *photos_list_albums_json(void);
|
||||
|
||||
char *photos_list_assets_json(const char *album_id);
|
||||
|
||||
char *photos_reverse_geocode_json(double latitude, double longitude);
|
||||
|
||||
char *photos_export_preview_json(
|
||||
const char *asset_id,
|
||||
const char *output_dir,
|
||||
|
||||
+140
-3
@@ -1,6 +1,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
#import <MapKit/MapKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#import <objc/message.h>
|
||||
#import "photokit_bridge.h"
|
||||
@@ -311,6 +313,87 @@ static NSString *media_type_string(PHAssetMediaType type) {
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *source_type_string(PHAssetSourceType type) {
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
if (type & PHAssetSourceTypeUserLibrary) [parts addObject:@"userLibrary"];
|
||||
if (type & PHAssetSourceTypeCloudShared) [parts addObject:@"cloudShared"];
|
||||
if (type & PHAssetSourceTypeiTunesSynced) [parts addObject:@"iTunesSynced"];
|
||||
return parts.count > 0 ? [parts componentsJoinedByString:@","] : @"unknown";
|
||||
}
|
||||
|
||||
static NSString *playback_style_string(PHAssetPlaybackStyle style) {
|
||||
switch (style) {
|
||||
case PHAssetPlaybackStyleImage: return @"image";
|
||||
case PHAssetPlaybackStyleImageAnimated: return @"imageAnimated";
|
||||
case PHAssetPlaybackStyleLivePhoto: return @"livePhoto";
|
||||
case PHAssetPlaybackStyleVideo: return @"video";
|
||||
case PHAssetPlaybackStyleVideoLooping: return @"videoLooping";
|
||||
default: return @"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static NSArray<NSString *> *media_subtype_strings(PHAssetMediaSubtype subtypes) {
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
if (subtypes & PHAssetMediaSubtypePhotoPanorama) [parts addObject:@"photoPanorama"];
|
||||
if (subtypes & PHAssetMediaSubtypePhotoHDR) [parts addObject:@"photoHDR"];
|
||||
if (subtypes & PHAssetMediaSubtypePhotoScreenshot) [parts addObject:@"photoScreenshot"];
|
||||
if (subtypes & PHAssetMediaSubtypePhotoLive) [parts addObject:@"photoLive"];
|
||||
if (subtypes & PHAssetMediaSubtypePhotoDepthEffect) [parts addObject:@"photoDepthEffect"];
|
||||
if (subtypes & PHAssetMediaSubtypeVideoStreamed) [parts addObject:@"videoStreamed"];
|
||||
if (subtypes & PHAssetMediaSubtypeVideoHighFrameRate) [parts addObject:@"videoHighFrameRate"];
|
||||
if (subtypes & PHAssetMediaSubtypeVideoTimelapse) [parts addObject:@"videoTimelapse"];
|
||||
return parts;
|
||||
}
|
||||
|
||||
static NSArray<NSString *> *burst_selection_type_strings(PHAssetBurstSelectionType types) {
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
if (types & PHAssetBurstSelectionTypeAutoPick) [parts addObject:@"autoPick"];
|
||||
if (types & PHAssetBurstSelectionTypeUserPick) [parts addObject:@"userPick"];
|
||||
return parts;
|
||||
}
|
||||
|
||||
static NSNumber *resource_file_size(PHAssetResource *res) {
|
||||
@try {
|
||||
id value = [res valueForKey:@"fileSize"];
|
||||
if ([value respondsToSelector:@selector(longLongValue)]) {
|
||||
return @([value longLongValue]);
|
||||
}
|
||||
} @catch (NSException *e) {}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static NSDictionary *location_dict(CLLocation *location) {
|
||||
if (!location) return nil;
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
dict[@"latitude"] = @(location.coordinate.latitude);
|
||||
dict[@"longitude"] = @(location.coordinate.longitude);
|
||||
dict[@"altitude"] = @(location.altitude);
|
||||
dict[@"horizontalAccuracy"] = @(location.horizontalAccuracy);
|
||||
return dict;
|
||||
}
|
||||
|
||||
static NSDictionary *editing_input_info(PHAsset *asset) {
|
||||
PHContentEditingInputRequestOptions *opts = [[PHContentEditingInputRequestOptions alloc] init];
|
||||
opts.networkAccessAllowed = NO;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
__block NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
[asset requestContentEditingInputWithOptions:opts completionHandler:^(PHContentEditingInput *input, NSDictionary *info) {
|
||||
if (input.adjustmentData) {
|
||||
dict[@"formatIdentifier"] = input.adjustmentData.formatIdentifier ?: @"";
|
||||
dict[@"formatVersion"] = input.adjustmentData.formatVersion ?: @"";
|
||||
}
|
||||
NSURL *url = input.fullSizeImageURL;
|
||||
if (url.lastPathComponent.length > 0) {
|
||||
dict[@"baseFilename"] = url.lastPathComponent;
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
if (!semaphore_wait_with_timeout(sem, 5)) {
|
||||
return nil;
|
||||
}
|
||||
return dict.count > 0 ? dict : nil;
|
||||
}
|
||||
|
||||
static NSString *iso8601_string(NSDate *date) {
|
||||
if (!date) return nil;
|
||||
static NSDateFormatter *fmt = nil;
|
||||
@@ -355,18 +438,22 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
NSString *mediaTypeStr = media_type_string(asset.mediaType);
|
||||
|
||||
NSString *creationDateStr = iso8601_string(asset.creationDate);
|
||||
NSString *modificationDateStr = iso8601_string(asset.modificationDate);
|
||||
|
||||
NSMutableArray *resourcesList = [NSMutableArray arrayWithCapacity:resources.count];
|
||||
for (PHAssetResource *res in resources) {
|
||||
NSString *resTypeStr = resource_type_string(res.type);
|
||||
NSString *uti = res.uniformTypeIdentifier ?: @"";
|
||||
BOOL isLocal = resource_is_locally_available(res);
|
||||
[resourcesList addObject:@{
|
||||
NSMutableDictionary *resourceDict = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
@"type": resTypeStr,
|
||||
@"filename": res.originalFilename ?: @"",
|
||||
@"uti": uti,
|
||||
@"local": @(isLocal)
|
||||
}];
|
||||
NSNumber *size = resource_file_size(res);
|
||||
if (size) resourceDict[@"size"] = size;
|
||||
[resourcesList addObject:resourceDict];
|
||||
}
|
||||
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
@@ -374,17 +461,37 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
@"filename": filename ?: @"",
|
||||
@"cloud": cloudStatus,
|
||||
@"mediaType": mediaTypeStr,
|
||||
@"mediaSubtypes": media_subtype_strings(asset.mediaSubtypes),
|
||||
@"sourceType": source_type_string(asset.sourceType),
|
||||
@"playbackStyle": playback_style_string(asset.playbackStyle),
|
||||
@"pixelWidth": @(asset.pixelWidth),
|
||||
@"pixelHeight": @(asset.pixelHeight),
|
||||
@"duration": @(asset.duration),
|
||||
@"isFavorite": @(asset.isFavorite)
|
||||
@"isFavorite": @(asset.isFavorite),
|
||||
@"isHidden": @(asset.isHidden),
|
||||
@"representsBurst": @(asset.representsBurst),
|
||||
@"burstSelectionTypes": burst_selection_type_strings(asset.burstSelectionTypes)
|
||||
}];
|
||||
if (creationDateStr) {
|
||||
dict[@"creationDate"] = creationDateStr;
|
||||
}
|
||||
if (modificationDateStr) {
|
||||
dict[@"modificationDate"] = modificationDateStr;
|
||||
}
|
||||
NSDictionary *loc = location_dict(asset.location);
|
||||
if (loc) {
|
||||
dict[@"location"] = loc;
|
||||
}
|
||||
if (asset.burstIdentifier.length > 0) {
|
||||
dict[@"burstIdentifier"] = asset.burstIdentifier;
|
||||
}
|
||||
if (@available(macOS 12, *)) {
|
||||
dict[@"hasAdjustments"] = @(asset.hasAdjustments);
|
||||
}
|
||||
NSDictionary *adjustmentInfo = editing_input_info(asset);
|
||||
if (adjustmentInfo) {
|
||||
dict[@"adjustmentInfo"] = adjustmentInfo;
|
||||
}
|
||||
if (resourcesList.count > 0) {
|
||||
dict[@"resources"] = resourcesList;
|
||||
}
|
||||
@@ -394,6 +501,36 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
return json_from_object(@{@"assets": list, @"total": @(assets.count)});
|
||||
}
|
||||
|
||||
char *photos_reverse_geocode_json(double latitude, double longitude) {
|
||||
if (@available(macOS 26.0, *)) {
|
||||
CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
|
||||
MKReverseGeocodingRequest *request = [[MKReverseGeocodingRequest alloc] initWithLocation:location];
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
__block MKMapItem *item = nil;
|
||||
__block NSError *mapErr = nil;
|
||||
[request getMapItemsWithCompletionHandler:^(NSArray<MKMapItem *> *mapItems, NSError *error) {
|
||||
mapErr = error;
|
||||
item = mapItems.firstObject;
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
if (!semaphore_wait_with_timeout(sem, 10)) {
|
||||
[request cancel];
|
||||
return json_from_object(@{@"error": @"timeout waiting for reverse geocode"});
|
||||
}
|
||||
if (mapErr || !item) {
|
||||
NSString *msg = mapErr.localizedDescription ?: @"reverse geocode failed";
|
||||
return json_from_object(@{@"error": msg});
|
||||
}
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
if (item.name) dict[@"name"] = item.name;
|
||||
if (item.address.fullAddress) dict[@"formattedAddress"] = item.address.fullAddress;
|
||||
if (item.address.shortAddress) dict[@"thoroughfare"] = item.address.shortAddress;
|
||||
return json_from_object(@{@"placemark": dict});
|
||||
}
|
||||
|
||||
return json_from_object(@{@"error": @"reverse geocoding requires macOS 26 or newer"});
|
||||
}
|
||||
|
||||
char *photos_list_tree_json(void) {
|
||||
PHFetchResult<PHCollection *> *collections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
|
||||
|
||||
@@ -709,4 +846,4 @@ int photos_get_progress_slot_count(void) {
|
||||
|
||||
void photos_reset_progress_slots(void) {
|
||||
memset(progress_slots, 0, sizeof(progress_slots));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ static int stub_access_rc = 0;
|
||||
static const char *stub_albums_json = "{\"albums\":[]}";
|
||||
static const char *stub_assets_json = "{\"assets\":[]}";
|
||||
static const char *stub_tree_json = "{\"collections\":[]}";
|
||||
static const char *stub_geocode_json = "{\"placemark\":{}}";
|
||||
static int stub_albums_null = 0;
|
||||
static int stub_assets_null = 0;
|
||||
static int stub_tree_null = 0;
|
||||
static int stub_geocode_null = 0;
|
||||
static int stub_cancelled = 0;
|
||||
static const char *stub_export_preview_json = NULL;
|
||||
static const char *stub_export_original_json = NULL;
|
||||
@@ -34,6 +36,8 @@ void photos_test_set_access(int rc) { stub_access_rc = rc; }
|
||||
void photos_test_set_albums(const char *json) { stub_albums_json = json; stub_albums_null = 0; }
|
||||
void photos_test_set_assets(const char *json) { stub_assets_json = json; stub_assets_null = 0; }
|
||||
void photos_test_set_tree(const char *json) { stub_tree_json = json; stub_tree_null = 0; }
|
||||
void photos_test_set_geocode(const char *json) { stub_geocode_json = json; stub_geocode_null = 0; }
|
||||
void photos_test_set_geocode_null(void) { stub_geocode_null = 1; }
|
||||
void photos_test_set_albums_null(void) { stub_albums_null = 1; }
|
||||
void photos_test_set_assets_null(void) { stub_assets_null = 1; }
|
||||
void photos_test_set_tree_null(void) { stub_tree_null = 1; }
|
||||
@@ -51,6 +55,13 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
return alloc_json(stub_assets_json);
|
||||
}
|
||||
|
||||
char *photos_reverse_geocode_json(double latitude, double longitude) {
|
||||
(void)latitude;
|
||||
(void)longitude;
|
||||
if (stub_geocode_null) return NULL;
|
||||
return alloc_json(stub_geocode_json);
|
||||
}
|
||||
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int quality, int index, int slot_index) {
|
||||
(void)asset_id;
|
||||
(void)output_dir;
|
||||
|
||||
Reference in New Issue
Block a user