It looks like images are forced to be in high quality (see line 321) but videos aren't. This commit forces videos to be in high quality format
443 lines
17 KiB
Swift
443 lines
17 KiB
Swift
import Foundation
|
|
import MobileCoreServices
|
|
import Flutter
|
|
import UIKit
|
|
import Photos
|
|
|
|
public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin {
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
let channel = FlutterMethodChannel(name: "photo_gallery", binaryMessenger: registrar.messenger())
|
|
let instance = SwiftPhotoGalleryPlugin()
|
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
}
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
if(call.method == "listAlbums") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let mediumType = arguments["mediumType"] as! String
|
|
result(listAlbums(mediumType: mediumType))
|
|
}
|
|
else if(call.method == "listMedia") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let albumId = arguments["albumId"] as! String
|
|
let mediumType = arguments["mediumType"] as! String
|
|
let skip = arguments["skip"] as? NSNumber
|
|
let take = arguments["take"] as? NSNumber
|
|
result(listMedia(albumId: albumId, skip: skip, take: take, mediumType: mediumType))
|
|
}
|
|
else if(call.method == "getMedium") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let mediumId = arguments["mediumId"] as! String
|
|
getMedium(
|
|
mediumId: mediumId,
|
|
completion: { (data: [String: Any?]?, error: Error?) -> Void in
|
|
result(data)
|
|
})
|
|
}
|
|
else if(call.method == "getThumbnail") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let mediumId = arguments["mediumId"] as! String
|
|
let width = arguments["width"] as? NSNumber
|
|
let height = arguments["height"] as? NSNumber
|
|
let highQuality = arguments["highQuality"] as? Bool
|
|
getThumbnail(
|
|
mediumId: mediumId,
|
|
width: width,
|
|
height: height,
|
|
highQuality: highQuality,
|
|
completion: { (data: Data?, error: Error?) -> Void in
|
|
result(data)
|
|
})
|
|
}
|
|
else if(call.method == "getAlbumThumbnail") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let albumId = arguments["albumId"] as! String
|
|
let width = arguments["width"] as? Int
|
|
let height = arguments["height"] as? Int
|
|
let highQuality = arguments["highQuality"] as? Bool
|
|
getAlbumThumbnail(
|
|
albumId: albumId,
|
|
width: width,
|
|
height: height,
|
|
highQuality: highQuality,
|
|
completion: { (data: Data?, error: Error?) -> Void in
|
|
result(data)
|
|
})
|
|
}
|
|
else if(call.method == "getFile") {
|
|
let arguments = call.arguments as! Dictionary<String, AnyObject>
|
|
let mediumId = arguments["mediumId"] as! String
|
|
getFile(
|
|
mediumId: mediumId,
|
|
completion: { (filepath: String?, error: Error?) -> Void in
|
|
result(filepath?.replacingOccurrences(of: "file://", with: ""))
|
|
})
|
|
}
|
|
else {
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
private var assetCollections : [PHAssetCollection] = []
|
|
|
|
private func listAlbums(mediumType: String) -> [NSDictionary] {
|
|
self.assetCollections = []
|
|
let fetchOptions = PHFetchOptions()
|
|
var total = 0
|
|
var albums = [NSDictionary]()
|
|
var albumIds = Set<String>()
|
|
|
|
func addCollection (collection: PHAssetCollection, hideIfEmpty: Bool) -> Void {
|
|
let kRecentlyDeletedCollectionSubtype = PHAssetCollectionSubtype(rawValue: 1000000201)
|
|
guard collection.assetCollectionSubtype != kRecentlyDeletedCollectionSubtype else { return }
|
|
|
|
// De-duplicate by id.
|
|
let albumId = collection.localIdentifier
|
|
guard !albumIds.contains(albumId) else { return }
|
|
albumIds.insert(albumId)
|
|
|
|
let options = PHFetchOptions()
|
|
options.predicate = self.predicateFromMediumType(mediumType: mediumType)
|
|
if #available(iOS 9, *) {
|
|
fetchOptions.fetchLimit = 1
|
|
}
|
|
let count = PHAsset.fetchAssets(in: collection, options: options).count
|
|
if(count > 0 || !hideIfEmpty) {
|
|
total+=count
|
|
self.assetCollections.append(collection)
|
|
albums.append([
|
|
"id": collection.localIdentifier,
|
|
"mediumType": mediumType,
|
|
"name": collection.localizedTitle ?? "Unknown",
|
|
"count": count,
|
|
])
|
|
}
|
|
}
|
|
|
|
func processPHAssetCollections(fetchResult: PHFetchResult<PHAssetCollection>, hideIfEmpty: Bool) -> Void {
|
|
fetchResult.enumerateObjects { (assetCollection, _, _) in
|
|
addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty)
|
|
}
|
|
}
|
|
|
|
func processPHCollections (fetchResult: PHFetchResult<PHCollection>, hideIfEmpty: Bool) -> Void {
|
|
fetchResult.enumerateObjects { (collection, _, _) in
|
|
if let assetCollection = collection as? PHAssetCollection {
|
|
addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty)
|
|
} else if let collectionList = collection as? PHCollectionList {
|
|
processPHCollections(fetchResult: PHCollectionList.fetchCollections(in: collectionList, options: nil), hideIfEmpty: hideIfEmpty)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Smart Albums.
|
|
processPHAssetCollections(
|
|
fetchResult: PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: fetchOptions),
|
|
hideIfEmpty: true
|
|
)
|
|
|
|
// User-created collections.
|
|
processPHCollections(
|
|
fetchResult: PHAssetCollection.fetchTopLevelUserCollections(with: fetchOptions),
|
|
hideIfEmpty: true
|
|
)
|
|
|
|
albums.insert([
|
|
"id": "__ALL__",
|
|
"mediumType": mediumType,
|
|
"name": "All",
|
|
"count" : countMedia(collection: nil, mediumTypes: [mediumType]),
|
|
], at: 0)
|
|
|
|
return albums
|
|
}
|
|
|
|
private func countMedia(collection: PHAssetCollection?, mediumTypes: [String]) -> Int {
|
|
let options = PHFetchOptions()
|
|
options.predicate = self.predicateFromMediumTypes(mediumTypes: mediumTypes)
|
|
if(collection == nil) {
|
|
return PHAsset.fetchAssets(with: options).count
|
|
}
|
|
|
|
return PHAsset.fetchAssets(in: collection!, options: options).count
|
|
}
|
|
|
|
private func listMedia(albumId: String, skip: NSNumber?, take: NSNumber?, mediumType: String) -> NSDictionary {
|
|
let fetchOptions = PHFetchOptions()
|
|
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
|
fetchOptions.predicate = predicateFromMediumType(mediumType: mediumType)
|
|
|
|
let collection = self.assetCollections.first(where: { (collection) -> Bool in
|
|
collection.localIdentifier == albumId
|
|
})
|
|
|
|
let fetchResult = albumId == "__ALL__"
|
|
? PHAsset.fetchAssets(with: fetchOptions)
|
|
: PHAsset.fetchAssets(in: collection!, options: fetchOptions)
|
|
let start = skip?.intValue ?? 0
|
|
let total = fetchResult.count
|
|
let end = take == nil ? total : min(start + take!.intValue, total)
|
|
var items = [[String: Any?]]()
|
|
for index in start..<end {
|
|
let asset = fetchResult.object(at: index) as PHAsset
|
|
items.append(getMediumFromAsset(asset: asset))
|
|
}
|
|
|
|
return [
|
|
"start": start,
|
|
"total": total,
|
|
"items": items,
|
|
]
|
|
}
|
|
|
|
private func getMedium(mediumId: String, completion: @escaping ([String : Any?]?, Error?) -> Void) {
|
|
let fetchOptions = PHFetchOptions()
|
|
if #available(iOS 9, *) {
|
|
fetchOptions.fetchLimit = 1
|
|
}
|
|
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
|
|
|
|
if (assets.count > 0) {
|
|
let asset: PHAsset = assets[0]
|
|
completion(getMediumFromAsset(asset: asset), nil)
|
|
return
|
|
}
|
|
|
|
completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
}
|
|
|
|
private func getThumbnail(
|
|
mediumId: String,
|
|
width: NSNumber?,
|
|
height: NSNumber?,
|
|
highQuality: Bool?,
|
|
completion: @escaping (Data?, Error?) -> Void
|
|
) {
|
|
let manager = PHImageManager.default()
|
|
let fetchOptions = PHFetchOptions()
|
|
if #available(iOS 9, *) {
|
|
fetchOptions.fetchLimit = 1
|
|
}
|
|
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
|
|
|
|
if (assets.count > 0) {
|
|
let asset: PHAsset = assets[0]
|
|
|
|
let options = PHImageRequestOptions()
|
|
options.deliveryMode = (highQuality ?? false)
|
|
? PHImageRequestOptionsDeliveryMode.highQualityFormat
|
|
: PHImageRequestOptionsDeliveryMode.fastFormat
|
|
options.isSynchronous = false
|
|
options.isNetworkAccessAllowed = true
|
|
options.version = .current
|
|
|
|
let imageSize = CGSize(width: width?.intValue ?? 128, height: height?.intValue ?? 128)
|
|
manager.requestImage(
|
|
for: asset,
|
|
targetSize: CGSize(width: imageSize.width * UIScreen.main.scale, height: imageSize.height * UIScreen.main.scale),
|
|
contentMode: PHImageContentMode.aspectFill,
|
|
options: options,
|
|
resultHandler: {(uiImage: UIImage?, info) in
|
|
guard let image = uiImage else {
|
|
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
return
|
|
}
|
|
let bytes = image.jpegData(compressionQuality: CGFloat(70))
|
|
completion(bytes, nil)
|
|
})
|
|
return
|
|
}
|
|
|
|
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
}
|
|
|
|
private func getAlbumThumbnail(
|
|
albumId: String,
|
|
width: Int?,
|
|
height: Int?,
|
|
highQuality: Bool?,
|
|
completion: @escaping (Data?, Error?) -> Void
|
|
) {
|
|
let manager = PHImageManager.default()
|
|
let fetchOptions = PHFetchOptions()
|
|
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
|
if #available(iOS 9, *) {
|
|
fetchOptions.fetchLimit = 1
|
|
}
|
|
|
|
let assets = albumId == "__ALL__" ?
|
|
PHAsset.fetchAssets(with: fetchOptions) :
|
|
PHAsset.fetchAssets(in: self.assetCollections.first(where: { (collection) -> Bool in
|
|
collection.localIdentifier == albumId
|
|
})!, options: fetchOptions)
|
|
|
|
if (assets.count > 0) {
|
|
let asset: PHAsset = assets[0]
|
|
|
|
let options = PHImageRequestOptions()
|
|
options.deliveryMode = (highQuality ?? false)
|
|
? PHImageRequestOptionsDeliveryMode.highQualityFormat
|
|
: PHImageRequestOptionsDeliveryMode.fastFormat
|
|
options.isSynchronous = false
|
|
options.isNetworkAccessAllowed = true
|
|
options.version = .current
|
|
|
|
let imageSize = CGSize(width: width ?? 128, height: height ?? 128)
|
|
manager.requestImage(
|
|
for: asset,
|
|
targetSize: CGSize(
|
|
width: imageSize.width * UIScreen.main.scale,
|
|
height: imageSize.height * UIScreen.main.scale
|
|
),
|
|
contentMode: PHImageContentMode.aspectFill,
|
|
options: options,
|
|
resultHandler: {(uiImage: UIImage?, info) in
|
|
guard let image = uiImage else {
|
|
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
return
|
|
}
|
|
let bytes = image.jpegData(compressionQuality: CGFloat(80))
|
|
completion(bytes, nil)
|
|
})
|
|
return
|
|
}
|
|
|
|
completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
}
|
|
|
|
private func getFile(mediumId: String, completion: @escaping (String?, Error?) -> Void) {
|
|
let manager = PHImageManager.default()
|
|
|
|
let fetchOptions = PHFetchOptions()
|
|
if #available(iOS 9, *) {
|
|
fetchOptions.fetchLimit = 1
|
|
}
|
|
let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions)
|
|
|
|
if (assets.count > 0) {
|
|
let asset: PHAsset = assets[0]
|
|
if(asset.mediaType == PHAssetMediaType.image) {
|
|
let options = PHImageRequestOptions()
|
|
options.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
|
|
options.isSynchronous = false
|
|
options.isNetworkAccessAllowed = true
|
|
options.version = .current
|
|
|
|
manager.requestImageData(
|
|
for: asset,
|
|
options: options,
|
|
resultHandler: { (data: Data?, uti: String?, orientation, info) in
|
|
DispatchQueue.main.async(execute: {
|
|
guard let originalData = data else {
|
|
completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil))
|
|
return
|
|
}
|
|
guard let jpgData = self.convertToJpeg(originalData: originalData) else {
|
|
completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil))
|
|
return
|
|
}
|
|
// Writing to file
|
|
let filepath = self.exportPathForAsset(asset: asset, ext: ".jpg")
|
|
try! jpgData.write(to: filepath, options: .atomic)
|
|
completion(filepath.absoluteString, nil)
|
|
})
|
|
})
|
|
} else if(asset.mediaType == PHAssetMediaType.video
|
|
|| asset.mediaType == PHAssetMediaType.audio) {
|
|
let options = PHVideoRequestOptions()
|
|
options.isNetworkAccessAllowed = true
|
|
options.deliveryMode = .highQualityFormat
|
|
options.version = .current
|
|
|
|
manager.requestAVAsset(forVideo: asset, options: options, resultHandler: { (avAsset, avAudioMix, info) in
|
|
DispatchQueue.main.async(execute: {
|
|
do {
|
|
let avAsset = avAsset as? AVURLAsset
|
|
let data = try Data(contentsOf: avAsset!.url)
|
|
let filepath = self.exportPathForAsset(asset: asset, ext: ".mov")
|
|
try! data.write(to: filepath, options: .atomic)
|
|
completion(filepath.absoluteString, nil)
|
|
} catch {
|
|
completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getMediumFromAsset(asset: PHAsset) -> [String: Any?] {
|
|
return [
|
|
"id": asset.localIdentifier,
|
|
"mediumType": toDartMediumType(value: asset.mediaType),
|
|
"height": asset.pixelHeight,
|
|
"width": asset.pixelWidth,
|
|
"duration": NSInteger(asset.duration * 1000),
|
|
"creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970 * 1000) : nil,
|
|
"modifiedDate": (asset.modificationDate != nil) ? NSInteger(asset.modificationDate!.timeIntervalSince1970 * 1000) : nil
|
|
]
|
|
}
|
|
|
|
/// Converts to JPEG, and keep EXIF data.
|
|
private func convertToJpeg(originalData: Data) -> Data? {
|
|
guard let image: UIImage = UIImage(data: originalData) else { return nil }
|
|
|
|
let originalSrc = CGImageSourceCreateWithData(originalData as CFData, nil)!
|
|
let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse]
|
|
let originalMetadata = CGImageSourceCopyPropertiesAtIndex(originalSrc, 0, options as CFDictionary)
|
|
|
|
guard let jpeg = image.jpegData(compressionQuality: 1.0) else { return nil }
|
|
|
|
let src = CGImageSourceCreateWithData(jpeg as CFData, nil)!
|
|
let data = NSMutableData()
|
|
let uti = CGImageSourceGetType(src)!
|
|
let dest = CGImageDestinationCreateWithData(data as CFMutableData, uti, 1, nil)!
|
|
CGImageDestinationAddImageFromSource(dest, src, 0, originalMetadata)
|
|
if !CGImageDestinationFinalize(dest) { return nil }
|
|
|
|
return data as Data
|
|
}
|
|
|
|
|
|
private func exportPathForAsset(asset: PHAsset, ext: String) -> URL {
|
|
let mediumId = asset.localIdentifier
|
|
.replacingOccurrences(of: "/", with: "__")
|
|
.replacingOccurrences(of: "\\", with: "__")
|
|
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
|
let tempFolder = paths[0].appendingPathComponent("photo_gallery")
|
|
try! FileManager.default.createDirectory(at: tempFolder, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
return paths[0].appendingPathComponent(mediumId+ext)
|
|
}
|
|
|
|
private func toSwiftMediumType(value: String) -> PHAssetMediaType? {
|
|
switch value {
|
|
case "image": return PHAssetMediaType.image
|
|
case "video": return PHAssetMediaType.video
|
|
case "audio": return PHAssetMediaType.audio
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
private func toDartMediumType(value: PHAssetMediaType) -> String? {
|
|
switch value {
|
|
case PHAssetMediaType.image: return "image"
|
|
case PHAssetMediaType.video: return "video"
|
|
case PHAssetMediaType.audio: return "audio"
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
private func predicateFromMediumTypes(mediumTypes: [String]) -> NSPredicate {
|
|
let predicates = mediumTypes.map { (dartValue) -> NSPredicate in
|
|
return predicateFromMediumType(mediumType: dartValue)
|
|
}
|
|
return NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.or, subpredicates: predicates)
|
|
}
|
|
|
|
private func predicateFromMediumType(mediumType: String) -> NSPredicate {
|
|
let swiftType = toSwiftMediumType(value: mediumType)
|
|
return NSPredicate(format: "mediaType = %d", swiftType!.rawValue)
|
|
}
|
|
}
|