Save image to photo album with datetime metadata - ios

I'm trying to use the metadata property in writeImageDataToSavedPhotosAlbum to save a GIF to the iPhone's photo album and add date/time metadata so that the saved GIF appears in a different place in the user's photo album instead of the default location at the end of the photo album.
I've tried doing the following:
let metadata: [String: AnyObject]! = [kCGImagePropertyTIFFDictionary as String:
[kCGImagePropertyTIFFDateTime as String: dateTime!],
kCGImagePropertyExifDictionary as String:
[kCGImagePropertyExifDateTimeDigitized as String: dateTime!,
kCGImagePropertyExifDateTimeOriginal as String: dateTime!]]
library.writeImageDataToSavedPhotosAlbum(data, metadata: metadata, completionBlock: completionBlock)
and the debug print for the metadata variable shows:
["{TIFF}": {
DateTime = "2015:10:09 20:07:48";
}, "{Exif}": {
DateTimeDigitized = "2015:10:09 20:07:48";
DateTimeOriginal = "2015:10:09 20:07:48";
}]
However, it doesn't seem like setting the metadata worked because the GIF is still saved at the end of the photo album with the current timestamp instead of the timestamp that I'm trying to set.

It appears the Photos app does not sort based on the date in the image's metadata (most likely because that metadata can be stored on iCloud, it's potentially not available locally). Instead it appears to sort by the creationDate, which is a property defined on PHAsset. You can change that using the Photos framework. Something like this should do the trick:
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
let request = PHAssetChangeRequest(forAsset: asset)
request.creationDate = dateTime!
}, completionHandler: { (success: Bool, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue()) {
//done
}
})
However, note that the Camera Roll or All Photos album does not sort them the same as the Years, Collections, and Moments view. For some reason Camera Roll/All Photos does not sort on the creationDate while Years, Collections, and Moments does. It doesn't use the photo metadata either, so I am not sure what it is examining to sort the photos for that album.

Related

iOS: How to extract thumbnail and metadata from photo file on disk

Apple doc here https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/capturing_still_and_live_photos/capturing_thumbnail_and_preview_images explains how to capture thubnails.
At the bottom, it says
If you requested an embedded thumbnail image, that image isn't
directly accessible from the AVCapturePhoto object—it's embedded in
the image file data that you get by calling the photo object's
fileDataRepresentation() method.
Seems impossible to separate the embedded thumbnail from the main photo. So what is the meaning of embedded thumbnail?
I want to save the AVCapturePhoto in JPG and raw DNG (requested embedded thumbnails for both) to App's Documents directory (I do not use PhotoKit) and then load it back to a UIImageView.
I save a photo like this:
if let data = capturePhoto.fileDataRepresentation() {
data.write(to: documentsPath, options: [])
}
And load it back to a UIImage like this:
if let data = FileManager.default.contents(at: path) {
let image = UIImage(data: data)
}
But it will be better to load the embedded thubmail first. If a user clicks to see the large image, load the full imgage file then.
I also want to show the metadata, e.g., GPS location, flash status, ISO, shutter speed etc. I wonder how to do that.
AVFoundation's AVAsset wraps some metadata types, but apparently not EXIF data. If you want the thumbnail you have to use the CoreGraphics framework. This function fetches the thumbnail if present, limiting the maximum side length to 512 pixels.
public func getImageThumbnail(url: URL) -> CGImage? {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
let thumbnailOptions: [String: Any] = [
kCGImageSourceCreateThumbnailWithTransform as String: true,
kCGImageSourceCreateThumbnailFromImageIfAbsent as String: false, // true will create if thumbnail not present
kCGImageSourceThumbnailMaxPixelSize as String: 512
]
return CGImageSourceCreateThumbnailAtIndex(imageSource, 0, thumbnailOptions as CFDictionary);
}
For all the rest of the metadata, you can use CGImageSourceCopyPropertiesAtIndex or CGImageSourceCopyMetadataAtIndex.

Share PHAsset stored in camera roll using UIActivityViewController

I'm storing images and videos in a Camera Roll album using PhotoKit, and want to allow the user to share them using UIActivityViewController. If I pass UIActivityViewController a UIImage instance, it works as expected, probably because the image data is passed in memory. However, videos need to be passed by URL because there's no video analogue to UIImage. When I pass a URL to a video, I get an error "Could not create sandbox extension". If I pass a URL to an image, I get a similar error.
Based on this, it seems as though I might be able to get around this error by exporting the assets to the Documents directory, and passing UIActivityViewController the URL to the asset in Documents. However, I've read elsewhere that the Camera Roll can serve a similar purpose, and it goes to reason that the Camera Roll would be one of the few places that can hold data for sharing between apps.
Is there a way to pass UIActivityViewController URLs to Camera Roll assets without copying them to Documents? Is there a better way to be sharing images and video that are already in Camera Roll?
Implementation Details:
I'm generating URLs for assets using this:
func videoFor(asset: PHAsset, resultHander: #escaping (AVAsset?, AVAudioMix?, [AnyHashable : Any]?) -> Void) {
imageManager.requestAVAsset(forVideo: asset, options: nil, resultHandler: resultHander)
}
func urlFor(asset: PHAsset, resultHandler: #escaping (URL?) -> Void) {
if ( asset.mediaType == .video ) {
videoFor(asset: asset) { (asset, audioMix, info) in
let asset = asset as! AVURLAsset
resultHandler(asset.url)
}
}
else if ( asset.mediaType == .image ) {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
asset.requestContentEditingInput(with: options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) -> Void in
resultHandler(contentEditingInput!.fullSizeImageURL as URL?)
})
}
else {
resultHandler(nil)
}
}
Here is the full error I get in console when trying to share an image by URL:
Failed to determine whether URL /var/mobile/Media/DCIM/100APPLE/IMG_0201.JPG (n) is managed by a file provider
Could not create sandbox extension. Error: Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedDescription=Could not create sandbox extension of type com.apple.app-sandbox.read for URL /var/mobile/Media/DCIM/100APPLE/IMG_0201.JPG. Error: No such file or directory}
... and for a video:
Failed to determine whether URL /var/mobile/Media/DCIM/100APPLE/IMG_0202.M4V (n) is managed by a file provider
Could not create sandbox extension. Error: Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedDescription=Could not create sandbox extension of type com.apple.app-sandbox.read for URL /var/mobile/Media/DCIM/100APPLE/IMG_0202.M4V. Error: Operation not permitted}
I was stuck on the same problem today. Here is my solution. Hope this helps or guides you to the right path.
PHImageManager.default().requestExportSession(forVideo: video, options: nil, exportPreset: AVAssetExportPresetPassthrough) { (exportSession, nil) in
if let exportSession = exportSession {
exportSession.outputURL = destinationURLForFile
exportSession.outputFileType = AVFileType.m4v
exportSession.exportAsynchronously() {
// Load the share sheet using destinationURLForFile
}
}
}
What this does is export the video to the provided location destinationURLForFile (i used the Documents directory. Make sure you delete the file if its already there otherwise the export MAY not work cause it may not override the file).
You can set the type based on available types. I needed m4v.
Then, export async and just call the share sheet or whatever sharing mechanism you have.

How to save JPEG/RAW image data to camera roll in iOS 11, can't access processed image data

I'm attempting to write a photoapp that can take both RAW and JPEG images and save them to the camera roll. The functions jpegPhotoDataRepresentation and dngPhotoDataRepresentation seem to be the key to all examples I've found, however both of these are deprecated in iOS 11 and the function for saving after "capturePhoto" is now
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
The main example I've been able to find of a working RAW iOS11 app is this:
https://ubunifu.co/swift/raw-photo-capture-sample-swift-4-ios-11
which works, however it only shoots RAW and saving is clumsy because it's not on the camera roll.
I've changed my photo settings to allow for both raw and processed capture with this line
photoSettings = AVCapturePhotoSettings(rawPixelFormatType: availableRawFormat.uint32Value, processedFormat: [AVVideoCodecKey : AVVideoCodecType.jpeg])
But once I've actually captured the photo I have no idea how to access the processedFormat data. fileDataRepresentation seems to be the only way to access the dng stuff, but there's no way to get at the jpeg separately? The code I've found from Apple pre-iOS11 suggests to use PHPhotoLibrary and add a resource, but this requires a data representation which I'm unable to access other than as a dng file, which when saved to the library is just white because the library is not able to handle RAW files. Here's my photoOutput code in case it helps.
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
let dir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! as String
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmss"
formatter.locale = Locale.init(identifier: "en_US_POSIX")
let filePath = dir.appending(String(format: "/%#.dng", formatter.string(from: Date())))
let dngFileURL = URL(fileURLWithPath: filePath)
let dngData = photo.fileDataRepresentation()!
do {
try dngData.write(to: dngFileURL, options: [])
} catch {
print("Unable to write DNG file.")
return
}
PHPhotoLibrary.shared().performChanges( {
let creationRequest = PHAssetCreationRequest.forAsset()
let creationOptions = PHAssetResourceCreationOptions()
creationOptions.shouldMoveFile = true
//dngData is the problem, this should be the jpeg representation
creationRequest.addResource(with: .photo, data: dngData, options: nil)
//This line works fine, the associated file is the correct RAW file, but the jpeg preview is garbage
creationRequest.addResource(with: .alternatePhoto, fileURL: dngFileURL, options: creationOptions)
}, completionHandler: nil)
}
Okay, following up on comment from earlier and the Apple docs on Capturing Photos in RAW Format:
As you’ve noticed, if you want to shoot RAW and save it in the Photos library, you need to save DNG+processed versions together in the same asset so that Photos library clients that don’t support RAW still have a readable version of the asset. (That includes the Photos app itself...) Saving both RAW+processed means specifying that in the capture.
If you’re requesting RAW+processed capture (where processed is JPEG, or even better, HEIF), you’re getting two photos for every shot you take. That means your didFinishProcessingPhoto callback gets called twice: once to deliver the JPEG (or HEIF), again to deliver the RAW.
Since you need to add RAW+processed versions of the asset to Photos together, you should wait until the capture output delivers both versions before trying to create the Photos asset. You’ll notice the code snippets in that Apple doc stash the data for both versions in the didFinishProcessingPhoto callback:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if photo.isRawPhoto {
// Save the RAW (DNG) fileDataRepresentation to a URL
} else {
// Hold JPEG/HEIF fileDataRepresentation in a property
}
}
Then, when the didFinishCaptureFor callback fires, they make sure they have both versions, and add them together to the Photos library.
Notice that when you add DNG and JPEG or HEIF versions of a photo together...
The JPEG/HEIF needs to be the primary photo resource, and the DNG the alternatePhoto resource.
You can add a JPEG/HEIF resource straight from Data in memory, but DNG needs to be added from a file URL.
So the Photos library part goes like this (again, inside the didFinishCaptureFor callback):
PHPhotoLibrary.shared().performChanges({
// Add the compressed (HEIF) data as the main resource for the Photos asset.
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: compressedData, options: nil)
// Add the RAW (DNG) file as an altenate resource.
let options = PHAssetResourceCreationOptions()
options.shouldMoveFile = true
creationRequest.addResource(with: .alternatePhoto, fileURL: rawURL, options: options)
}, completionHandler: self.handlePhotoLibraryError)
You can make CGImage extension like here and then get pixel buffer from that cgImage.
if let cgImage = photo.cgImageRepresentation() {
let pixelBuffer = cgImage.pixelBuffer()
}

Saving Already Created Live Photos

I've got some live photos created JPEG and MOV files, now i want to import them into the app that would allow the user to save the live photos to their photo library. How can i go about doing this?
I've looked into this: https://github.com/genadyo/LivePhotoDemoSwift Which basically allows you to record video and turn it into a live photo. But since i've already created the "live photos", can i save them to the camera roll right away or do i need to follow a different route?
You can create a LivePhoto from separate elements from a LivePhoto by using PHLivePhoto.requestLivePhotoWithResourceFileURLs, you will then be able to save it to the library.
func makeLivePhotoFromItems(imageURL: NSURL, videoURL: NSURL, previewImage: UIImage, completion: (livePhoto: PHLivePhoto) -> Void) {
PHLivePhoto.requestLivePhotoWithResourceFileURLs([imageURL, videoURL], placeholderImage: previewImage, targetSize: CGSizeZero, contentMode: PHImageContentMode.AspectFit) {
(livePhoto, infoDict) -> Void in
if let lp = livePhoto {
completion(livePhoto: lp)
}
}
}
makeLivePhotoFromItems(imgURL, videoURL: movURL, previewImage: prevImg) { (livePhoto) -> Void in
// "livePhoto" is your PHLivePhoto object, save it/use it here
}
You will need the JPEG file URL, the MOV file URL, and a "preview" image (which is usually just the JPEG or a lighter version of it).
Full example working in a Playground here.

Fetch photos from camera roll which are clicked by the camera using PHPhotoLibrary

I am using PHPhotoLibrary to access camera roll photos. But it is getting all images, like downloaded images, screenshots, Facebook Image etc. I need images which are clicked by the camera.
I believe that this link may help you:
How to get only images in the camera roll using Photos Framework
Through some experimentation we discovered a hidden property not
listed in the documentation (assetSource). Basically you have to do a
regular fetch request, then use a predicate to filter the ones from
the camera roll. This value should be 3.
Sample code:
//fetch all assets, then sub fetch only the range we need
var assets = PHAsset.fetchAssetsWithMediaType(PHAssetMediaType.Image, options: fetchOptions)
assets.enumerateObjectsUsingBlock { (obj, idx, bool) -> Void in
results.addObject(obj)
}
var cameraRollAssets = results.filteredArrayUsingPredicate(NSPredicate(format: "assetSource == %#", argumentArray: [3]))
results = NSMutableArray(array: cameraRollAssets)

Resources