diff --git a/FileProvider.podspec b/FileProvider.podspec index ace9359..3857906 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.8.3" + s.version = "0.9.0" s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and macOS." # This description is used to generate tags and improve search results. diff --git a/FileProvider.xcodeproj/project.pbxproj b/FileProvider.xcodeproj/project.pbxproj index 0dd041a..54aa1cc 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -92,6 +92,23 @@ 799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; }; 799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; }; 799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; }; + 79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; }; + 79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; }; + 79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; }; + 79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A71E2CC2940035128C /* CoreGraphics.framework */; }; + 79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A91E2CC2BB0035128C /* AVFoundation.framework */; }; + 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AB1E2CC2C20035128C /* ImageIO.framework */; }; + 79BD63AE1E2CC2EB0035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */; }; + 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AF1E2CC3300035128C /* libxml2.tbd */; }; + 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B11E2CC3350035128C /* ImageIO.framework */; }; + 79BD63B41E2CC33D0035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B31E2CC33D0035128C /* MediaPlayer.framework */; }; + 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B51E2CC3860035128C /* CoreFoundation.framework */; }; + 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B71E2CC38D0035128C /* AVFoundation.framework */; }; + 79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B91E2CC39B0035128C /* libxml2.tbd */; }; + 79BD63BC1E2CC3B90035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */; }; + 79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; }; + 79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */; }; + 79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63C11E2CC3D30035128C /* AVFoundation.framework */; }; 79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; @@ -134,6 +151,21 @@ 799396A31D48C02300086753 /* SMB2Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Types.swift; sourceTree = ""; }; 799396A41D48C02300086753 /* SMBErrorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMBErrorType.swift; sourceTree = ""; }; 799396A61D48C02300086753 /* WebDAVFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebDAVFileProvider.swift; sourceTree = ""; }; + 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedLocalFileProvider.swift; sourceTree = ""; }; + 79BD63A71E2CC2940035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63A91E2CC2BB0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63AB1E2CC2C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63AF1E2CC3300035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; + 79BD63B11E2CC3350035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; + 79BD63B31E2CC33D0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; + 79BD63B51E2CC3860035128C /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + 79BD63B71E2CC38D0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 79BD63B91E2CC39B0035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; }; + 79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; 79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -142,6 +174,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */, + 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */, + 79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */, + 79BD63AE1E2CC2EB0035128C /* MediaPlayer.framework in Frameworks */, 791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -150,6 +186,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */, + 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */, + 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */, + 79BD63B41E2CC33D0035128C /* MediaPlayer.framework in Frameworks */, + 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -157,6 +198,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */, + 79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */, + 79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */, + 79BD63BC1E2CC3B90035128C /* MediaPlayer.framework in Frameworks */, + 79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -166,6 +212,20 @@ 791950F31DE58A5300B4426E /* Frameworks */ = { isa = PBXGroup; children = ( + 79BD63C11E2CC3D30035128C /* AVFoundation.framework */, + 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */, + 79BD63BD1E2CC3C20035128C /* ImageIO.framework */, + 79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */, + 79BD63B91E2CC39B0035128C /* libxml2.tbd */, + 79BD63B71E2CC38D0035128C /* AVFoundation.framework */, + 79BD63B51E2CC3860035128C /* CoreFoundation.framework */, + 79BD63B31E2CC33D0035128C /* MediaPlayer.framework */, + 79BD63B11E2CC3350035128C /* ImageIO.framework */, + 79BD63AF1E2CC3300035128C /* libxml2.tbd */, + 79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */, + 79BD63AB1E2CC2C20035128C /* ImageIO.framework */, + 79BD63A91E2CC2BB0035128C /* AVFoundation.framework */, + 79BD63A71E2CC2940035128C /* CoreGraphics.framework */, 791950F41DE58A5400B4426E /* libxml2.tbd */, ); name = Frameworks; @@ -186,6 +246,7 @@ 7993965B1D48B7BF00086753 = { isa = PBXGroup; children = ( + 79E34A101E2AC6C600E1293B /* Extra */, 799396911D48C02300086753 /* Sources */, 7993968A1D48B8C700086753 /* Pod */, 799396681D48B7F600086753 /* Products */, @@ -225,6 +286,7 @@ 79F5745A1DFDB10A00179ABF /* FileObject.swift */, 799396961D48C02300086753 /* LocalFileProvider.swift */, 792572401DF23BDA006A1526 /* LocalHelper.swift */, + 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */, 7902C0851D61B56D00564440 /* RemoteSession.swift */, 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */, 799396971D48C02300086753 /* SMBClient.swift */, @@ -254,6 +316,13 @@ path = SMBTypes; sourceTree = ""; }; + 79E34A101E2AC6C600E1293B /* Extra */ = { + isa = PBXGroup; + children = ( + ); + name = Extra; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -428,6 +497,7 @@ 799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */, 799396CB1D48C02300086753 /* SMB2Query.swift in Sources */, 799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */, + 79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */, 7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */, 7924B1A51D89DAE000589DB7 /* Parser.swift in Sources */, 799396B01D48C02300086753 /* FileProvider.swift in Sources */, @@ -464,6 +534,7 @@ 799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */, 799396CC1D48C02300086753 /* SMB2Query.swift in Sources */, 7924B1AD1D89F7D800589DB7 /* SMBClient.swift in Sources */, + 79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */, 799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */, 7924B1A61D89DAE000589DB7 /* Parser.swift in Sources */, 799396B11D48C02300086753 /* FileProvider.swift in Sources */, @@ -500,6 +571,7 @@ 799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */, 799396CD1D48C02300086753 /* SMB2Query.swift in Sources */, 7924B1AE1D89F7D900589DB7 /* SMBClient.swift in Sources */, + 79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */, 799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */, 7924B1A71D89DAE000589DB7 /* Parser.swift in Sources */, 799396B21D48C02300086753 /* FileProvider.swift in Sources */, @@ -694,6 +766,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = A; @@ -749,6 +822,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = A; diff --git a/README.md b/README.md index a987ae1..dc0aaca 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,41 @@ documentsProvider.unregisterNotifcation(path: provider.currentPath) * **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification. +### Thumbnail and meta-information + +Providers which conform `ExtendedFileProvider` are able to generate thumbnail or provide file meta-information for images, media and pdf files. + +Local and Dropbox providers support this functionality. + +##### Thumbnails +To check either file thumbnail is supported or not and fetch thumbnail, use (and modify) these example code: + +```swift +let path = "/newImage.jpg" +let thumbSize = CGSize(width: 64, height: 64) +if documentsProvider.thumbnailOfFileSupported(path: path { + documentsProvider..thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in + DispatchQueue.main.async { + self.previewImage.image = image + } + } +} +``` + +##### Meta-informations + +To get meta-information like image/video taken date, dimension, etc., use (and modify) these example code: + +```swift +if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (propertiesDictionary, keys, error) in + for key in keys { + print("\(key): \(propertiesDictionary[key])") + } +} +``` + +* **Bonus:** You can modify/extend Local provider generator by setting `LocalFileInformationGenerator` static variables and methods + ## Contribute We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info. diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 9d2566e..bd86914 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -340,32 +340,43 @@ extension DropboxFileProvider: ExtendedFileProvider { switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": return true - /*case "doc", "docx", "docm", "xls", "xlsx", "xlsm": + case "doc", "docx", "docm", "xls", "xlsx", "xlsm": return true case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": return true case "rtf": - return true*/ + return true default: return false } } public func propertiesOfFileSupported(path: String) -> Bool { - return false + let fileExt = (path as NSString).pathExtension.lowercased() + switch fileExt { + case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": + return true + /*case "mp3", "aac", "m4a": + return true*/ + case "mp4", "mpg", "3gp", "mov", "avi": + return true + default: + return false + } } - - public func thumbnailOfFile(path: String, dimension: CGSize, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + + /// Default value for dimension is 64x64, according to Dropbox documentation + public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { let url: URL switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": url = URL(string: "https://content.dropboxapi.com/2/files/get_thumbnail")! - /*case "doc", "docx", "docm", "xls", "xlsx", "xlsm": + case "doc", "docx", "docm", "xls", "xlsx", "xlsm": fallthrough case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": fallthrough case "rtf": - url = NSURL(string: "https://content.dropboxapi.com/2/files/get_preview")!*/ + url = URL(string: "https://content.dropboxapi.com/2/files/get_preview")! default: return } @@ -373,7 +384,9 @@ extension DropboxFileProvider: ExtendedFileProvider { request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") var requestDictionary = ["path": path as NSString] requestDictionary["format"] = "jpeg" as NSString - requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString + if let dimension = dimension { + requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString + } request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg") let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in var image: ImageClass? = nil @@ -383,7 +396,13 @@ extension DropboxFileProvider: ExtendedFileProvider { } } if let data = data { - image = ImageClass(data: data) + if DropboxFileProvider.dataIsPDF(data) { + image = DropboxFileProvider.convertToImage(pdfData: data) + } else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") { + // TODO: Implement converting html returned type of get_preview to image + } else { + image = ImageClass(data: data) + } } completionHandler(image, error) }) @@ -391,7 +410,27 @@ extension DropboxFileProvider: ExtendedFileProvider { } public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { - NotImplemented() + let url = URL(string: "https://api.dropboxapi.com/2/files/get_metadata")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let requestDictionary = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)] + request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var dbError: FileProviderDropboxError? + var dic = [String: Any]() + var keys = [String]() + if let response = response as? HTTPURLResponse { + let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) + dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil + if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let properties = json["media_info"] as? [String: Any] { + (dic, keys) = self.mapMediaInfo(properties) + } + } + completionHandler(dic, keys, dbError ?? error) + }) + task.resume() } } diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index 42e1ae7..e922c98 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -210,6 +210,37 @@ internal extension DropboxFileProvider { return fileObject } + static let dateFormatter = DateFormatter() + static let decimalFormatter = NumberFormatter() + + func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) { + var dic = [String: Any]() + var keys = [String]() + if let dimensions = json["dimensions"] as? [String: Any], let height = dimensions["height"] as? UInt64, let width = dimensions["width"] as? UInt64 { + keys.append("Dimensions") + dic["Dimensions"] = "\(width)x\(height)" + } + if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double { + + DropboxFileProvider.decimalFormatter.numberStyle = .decimal + DropboxFileProvider.decimalFormatter.maximumFractionDigits = 5 + keys.append("Location") + let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude)) + let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude)) + dic["Location"] = "\(latStr), \(longStr)" + } + if let timeTakenStr = json["time_taken"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) { + keys.append("Date taken") + DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken) + } + if let duration = json["duration"] as? UInt64 { + keys.append("Duration") + dic["Duration"] = DropboxFileProvider.formatshort(interval: TimeInterval(duration)) + } + return (dic, keys) + } + func delegateNotify(_ operation: FileOperationType, error: Error?) { DispatchQueue.main.async(execute: { if error == nil { diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift new file mode 100644 index 0000000..c04b49f --- /dev/null +++ b/Sources/ExtendedLocalFileProvider.swift @@ -0,0 +1,444 @@ +// +// ExtendedLocalFileProvider.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2017 Mousavian. Distributed under MIT license. +// + +import Foundation +import ImageIO +import CoreGraphics +import AVFoundation +import MediaPlayer +#if os(iOS) || os(tvOS) +import UIKit +#elseif os(OSX) +import Cocoa +#endif + +extension LocalFileProvider: ExtendedFileProvider { + + public func thumbnailOfFileSupported(path: String) -> Bool { + switch (path as NSString).pathExtension.lowercased() { + case LocalFileInformationGenerator.imageThumbnailExtensions: + return true + case LocalFileInformationGenerator.audioThumbnailExtensions: + return true + case LocalFileInformationGenerator.videoThumbnailExtensions: + return true + case LocalFileInformationGenerator.pdfThumbnailExtensions: + return true + case LocalFileInformationGenerator.officeThumbnailExtensions: + return true + case LocalFileInformationGenerator.customThumbnailExtensions: + return true + default: + return false + } + } + + public func propertiesOfFileSupported(path: String) -> Bool { + let fileExt = (path as NSString).pathExtension.lowercased() + switch fileExt { + case LocalFileInformationGenerator.imagePropertiesExtensions: + return LocalFileInformationGenerator.imageProperties != nil + case LocalFileInformationGenerator.audioPropertiesExtensions: + return LocalFileInformationGenerator.audioProperties != nil + case LocalFileInformationGenerator.videoPropertiesExtensions: + return LocalFileInformationGenerator.videoProperties != nil + case LocalFileInformationGenerator.pdfPropertiesExtensions: + return LocalFileInformationGenerator.pdfProperties != nil + case LocalFileInformationGenerator.archivePropertiesExtensions: + return LocalFileInformationGenerator.archiveProperties != nil + case LocalFileInformationGenerator.officePropertiesExtensions: + return LocalFileInformationGenerator.officeProperties != nil + case LocalFileInformationGenerator.customPropertiesExtensions: + return LocalFileInformationGenerator.customProperties != nil + + default: + return false + } + } + + public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + (dispatch_queue).async { + var thumbnailImage: ImageClass? = nil + // Check cache + let fileURL = self.absoluteURL(path) + // Create Thumbnail and cache + switch fileURL.pathExtension.lowercased() { + case LocalFileInformationGenerator.videoThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL) + case LocalFileInformationGenerator.audioThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL) + case LocalFileInformationGenerator.imageThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL) + case LocalFileInformationGenerator.pdfThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL) + case LocalFileInformationGenerator.officeThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL) + case LocalFileInformationGenerator.customThumbnailExtensions: + thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL) + default: + completionHandler(nil, nil) + return + } + + if let image = thumbnailImage { + let scaledImage = dimension != nil ? LocalFileProvider.scaleDown(image: image, toSize: dimension!) : image + completionHandler(scaledImage, nil) + } + } + } + + public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) { + (dispatch_queue).async { + let fileExt = (path as NSString).pathExtension.lowercased() + var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? + switch fileExt { + case LocalFileInformationGenerator.imagePropertiesExtensions: + getter = LocalFileInformationGenerator.imageProperties + case LocalFileInformationGenerator.audioPropertiesExtensions: + getter = LocalFileInformationGenerator.audioProperties + case LocalFileInformationGenerator.videoPropertiesExtensions: + getter = LocalFileInformationGenerator.videoProperties + case LocalFileInformationGenerator.pdfPropertiesExtensions: + getter = LocalFileInformationGenerator.pdfProperties + case LocalFileInformationGenerator.archivePropertiesExtensions: + getter = LocalFileInformationGenerator.archiveProperties + case LocalFileInformationGenerator.officePropertiesExtensions: + getter = LocalFileInformationGenerator.officeProperties + case LocalFileInformationGenerator.customPropertiesExtensions: + getter = LocalFileInformationGenerator.customProperties + default: + break + } + + var dic = [String: Any]() + var keys = [String]() + if let getterMethod = getter { + (dic, keys) = getterMethod(self.absoluteURL(path)) + } + + completionHandler(dic, keys, nil) + } + + } +} + +public struct LocalFileInformationGenerator { + static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"] + static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"] + static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"] + static public var pdfThumbnailExtensions: [String] = ["pdf"] + static public var officeThumbnailExtensions: [String] = [] + static public var customThumbnailExtensions: [String] = [] + + static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"] + static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"] + static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"] + static public var pdfPropertiesExtensions: [String] = ["pdf"] + static public var archivePropertiesExtensions: [String] = [] + static public var officePropertiesExtensions: [String] = [] + static public var customPropertiesExtensions: [String] = [] + + static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + return ImageClass(contentsOfFile: fileURL.path) + } + + static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + let playerItem = AVPlayerItem(url: fileURL) + let metadataList = playerItem.asset.commonMetadata + for item in metadataList { + if item.commonKey == AVMetadataCommonKeyArtwork { + if let data = item.dataValue { + return ImageClass(data: data) + } + } + } + return nil + } + + static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + let asset = AVAsset(url: fileURL) + let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale) + if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) { + #if os(OSX) + return ImageClass(cgImage: cgImage, size: NSSize.zero) + #else + return ImageClass(cgImage: cgImage) + #endif + } + return nil + } + + static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + guard let data = try? Data(contentsOf: fileURL) else { return nil } + return LocalFileProvider.convertToImage(pdfData: data) + } + + static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + return nil + } + + static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + return nil + } + + static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in + func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) { + var x = top + var y = bottom + while (y != 0) { + let buffer = y + y = x % y + x = buffer + } + let hcfVal = x + let newTopVal = top/hcfVal + let newBottomVal = bottom/hcfVal + return(Int(newTopVal), Int(newBottomVal)) + } + + var dic = [String: Any]() + var keys = [String]() + guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else { + return (dic, keys) + } + let imageDict = cfImageDict as NSDictionary + let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? [String : AnyObject] ?? [:] + let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? [String : AnyObject] ?? [:] + if let pixelWidth: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber { + keys.append("Dimensions") + dic["Dimensions"] = "\(pixelWidth)x\(pixelHeight)" + } + if let dpi = imageDict[kCGImagePropertyDPIWidth as String] { + keys.append("DPI") + dic["DPI"] = dpi + } + if let devicemake = tiffDict[kCGImagePropertyTIFFMake as String] { + keys.append("Device make") + dic["Device make"] = devicemake + } + if let devicemodel = tiffDict[kCGImagePropertyTIFFModel as String] { + keys.append("Device model") + dic["Device model"] = devicemodel + } + if let lensmodel = exifDict[kCGImagePropertyExifLensModel as String] { + keys.append("Lens model") + dic["Lens model"] = lensmodel + } + if let artist = tiffDict[kCGImagePropertyTIFFArtist as String] as? String , !artist.isEmpty { + keys.append("Artist") + dic["Artist"] = artist + } + if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty { + keys.append("Copyright") + dic["Copyright"] = cr + } + if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty { + keys.append("Date taken") + dic["Date taken"] = date + } + if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String]?.doubleValue, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String]?.doubleValue { + keys.append("Location") + dic["Location"] = "\(latitude), \(longitude)" + } + if let colorspace = imageDict[kCGImagePropertyColorModel as String] { + keys.append("Color space") + dic["Color space"] = colorspace + } + if let focallen = exifDict[kCGImagePropertyExifFocalLength as String] { + keys.append("Focal length") + dic["Focal length"] = focallen + } + if let fnum = exifDict[kCGImagePropertyExifFNumber as String] { + keys.append("F number") + dic["F number"] = fnum + } + if let expprog = exifDict[kCGImagePropertyExifExposureProgram as String] { + keys.append("Exposure program") + dic["Exposure program"] = expprog + } + if let exp = exifDict[kCGImagePropertyExifExposureTime as String]?.doubleValue { + let expfrac = simplify(Int64(exp * 10_000_000_000_000), 10_000_000_000_000) + keys.append("Exposure time") + dic["Exposure time"] = "\(expfrac.newTop)/\(expfrac.newBottom)" + } + if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 { + keys.append("ISO speed") + dic["ISO speed"] = iso[0] + } + return (dic, keys) + } + + static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in + func makeDescription(_ key: String?) -> String? { + guard let key = key else { + return nil + } + guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: NSRegularExpression.Options()) else { + return nil + } + let newKey = regex.stringByReplacingMatches(in: key, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, (key as NSString).length) , withTemplate: "$1 $2") + return newKey.capitalized + } + + var dic = [String: Any]() + var keys = [String]() + if FileManager.default.fileExists(atPath: fileURL.path) { + let playerItem = AVPlayerItem(url: fileURL) + let metadataList = playerItem.asset.commonMetadata + for item in metadataList { + if let description = makeDescription(item.commonKey) { + if let value = item.stringValue { + keys.append(description) + dic[description] = value + } + } + } + if let ap = try? AVAudioPlayer(contentsOf: fileURL) { + keys.append("Duration") + dic["Duration"] = LocalFileProvider.formatshort(interval: ap.duration) + if let bitRate = ap.settings[AVSampleRateKey] as? Int { + keys.append("Bitrate") + dic["Bitrate"] = bitRate + } + } + } + return (dic, keys) + } + + static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in + var dic = [String: Any]() + var keys = [String]() + if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) { + dic = audioprops.prop + keys = audioprops.keys + dic.removeValue(forKey: "Duration") + if let index = keys.index(of: "Duration") { + keys.remove(at: index) + } + } + let asset = AVURLAsset(url: fileURL, options: nil) + let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo) + if videoTracks.count > 0 { + var bitrate: Float = 0 + let width = Int(videoTracks[0].naturalSize.width) + let height = Int(videoTracks[0].naturalSize.height) + keys.append("Dimensions") + dic["Dimensions"] = "\(width)x\(height)" + var duration: Int64 = 0 + for track in videoTracks { + duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0 + bitrate += track.estimatedDataRate + } + keys.append("Duration") + dic["Duration"] = LocalFileProvider.formatshort(interval: TimeInterval(duration)) + keys.append("Video Bitrate") + dic["Video Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps" + } + let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio) + // dic["Audio channels"] = audioTracks.count + var bitrate: Float = 0 + for track in audioTracks { + bitrate += track.estimatedDataRate + } + keys.append("Audio Bitrate") + dic["Audio Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps" + return (dic, keys) + } + + static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in + func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? { + var cfValue: CGPDFStringRef? = nil + if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) { + return value as String + } + return nil + } + + func convertDate(_ date: String) -> Date? { + var dateStr = date + if dateStr.hasPrefix("D:") { + dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2)) + } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMddHHmmssTZD" + if let result = dateFormatter.date(from: dateStr) { + return result + } + dateFormatter.dateFormat = "yyyyMMddHHmmss" + if let result = dateFormatter.date(from: dateStr) { + return result + } + return nil + } + + var dic = [String: Any]() + var keys = [String]() + if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info { + if let title = getKey("Title", from: dict), !title.isEmpty { + keys.append("Title") + dic["Title"] = title + } + if let author = getKey("Author", from: dict), !author.isEmpty { + keys.append("Author") + dic["Author"] = author + } + if let subject = getKey("Subject", from: dict), !subject.isEmpty { + keys.append("Subject") + dic["Subject"] = subject + } + var majorVersion: Int32 = 0 + var minorVersion: Int32 = 0 + reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion) + if majorVersion > 0 { + keys.append("Version") + dic["Version"] = String(majorVersion) + "." + String(minorVersion) + } + if reference.numberOfPages > 0 { + keys.append("Pages") + dic["Pages"] = reference.numberOfPages + } + + if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) { + let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size + keys.append("Resolution") + dic["Resolution"] = "\(Int(size.width))x\(Int(size.height))" + } + if let creator = getKey("Creator", from: dict), !creator.isEmpty { + keys.append("Content creator") + dic["Content creator"] = creator + } + if let creationDateString = getKey("CreationDate", from: dict), let creationDate = convertDate(creationDateString) { + keys.append("Creation date") + dic["Creation date"] = creationDate + } + if let modifiedDateString = getKey("ModDate", from: dict), let modDate = convertDate(modifiedDateString) { + keys.append("Modified date") + dic["Modified date"] = modDate + } + keys.append("Security") + dic["Security"] = reference.isEncrypted ? "Present" : "None" + keys.append("Allows printing") + dic["Allows printing"] = reference.allowsPrinting ? "Yes" : "No" + keys.append("Allows copying") + dic["Allows copying"] = reference.allowsCopying ? "Yes" : "No" + } + return (dic, keys) + } + + static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil + + static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil + + static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil +} + +func ~=(array: [T], value: T) -> Bool { + return array.contains(value) +} diff --git a/Sources/FPSStreamTask.swift b/Sources/FPSStreamTask.swift index 968bf35..efeb092 100644 --- a/Sources/FPSStreamTask.swift +++ b/Sources/FPSStreamTask.swift @@ -401,8 +401,8 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate { if #available(iOS 9.0, OSX 10.11, *) { _underlyingTask!.startSecureConnection() } else { - inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) - outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) + inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey) + outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey) } } @@ -414,8 +414,8 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate { if #available(iOS 9.0, OSX 10.11, *) { _underlyingTask!.stopSecureConnection() } else { - inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) - outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) + inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey) + outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey) } } diff --git a/Sources/FileProvider.h b/Sources/FileProvider.h index a7eb5ed..e89f9c3 100644 --- a/Sources/FileProvider.h +++ b/Sources/FileProvider.h @@ -5,6 +5,9 @@ // Created by Amir Abbas Mousavian on 5/6/95. // // + +#import + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR #import //! Project version number for FileProvider iOS. diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index a1faa60..3080f76 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -308,13 +308,137 @@ extension FileProviderBasic { } -public protocol ExtendedFileProvider: FileProvider { +public protocol ExtendedFileProvider: FileProviderBasic { func thumbnailOfFileSupported(path: String) -> Bool func propertiesOfFileSupported(path: String) -> Bool - func thumbnailOfFile(path: String, dimension: CGSize, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) + func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) + func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) } +extension ExtendedFileProvider { + public func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler) + } + + internal static func formatshort(interval: TimeInterval) -> String { + var result = "0:00" + if interval < TimeInterval(Int32.max) { + result = "" + var time = DateComponents() + time.hour = Int(interval / 3600) + time.minute = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60) + time.second = Int(interval.truncatingRemainder(dividingBy: 60)) + let formatter = NumberFormatter() + formatter.paddingCharacter = "0" + formatter.minimumIntegerDigits = 2 + formatter.maximumFractionDigits = 0 + let formatterFirst = NumberFormatter() + formatterFirst.maximumFractionDigits = 0 + if time.hour! > 0 { + result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)" + } else { + result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)" + } + } + result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": ")) + return result + } + + internal static func dataIsPDF(_ data: Data) -> Bool { + return data.count > 4 && data.scanString(length: 4, encoding: .ascii) == "%PDF" + } + + internal static func convertToImage(pdfData: Data?) -> ImageClass? { + guard let pdfData = pdfData else { return nil } + + let cfPDFData: CFData = pdfData as CFData + if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: 1) { + let frame = pageRef.getBoxRect(CGPDFBox.mediaBox) + var size = frame.size + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + + #if os(OSX) + let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not + + size.width *= CGFloat(ppp) + size.height *= CGFloat(ppp) + + let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, + bytesPerRow: 0, bitsPerPixel: 0) + + guard let context = NSGraphicsContext(bitmapImageRep: rep!) else { + return nil + } + + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.setCurrent(context) + + let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true) + context.cgContext.concatenate(transform) + + context.cgContext.translateBy(x: 0, y: size.height) + context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp)) + context.cgContext.drawPDFPage(pageRef) + + let resultingImage = NSImage(size: size) + resultingImage.addRepresentation(rep!) + return resultingImage + #else + let ppp = Int(UIScreen.main.scale) // fetch device is retina or not + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + size.width *= CGFloat(ppp) + size.height *= CGFloat(ppp) + UIGraphicsBeginImageContext(size) + + context.saveGState() + let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true) + context.concatenate(transform) + + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp)) + context.drawPDFPage(pageRef) + + context.restoreGState() + let resultingImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return resultingImage + #endif + } + return nil + } + + internal static func scaleDown(image: ImageClass, toSize maxSize: CGSize) -> ImageClass { + let height, width: CGFloat + if image.size.width > image.size.height { + width = maxSize.width + height = (image.size.height / image.size.width) * width + } else { + height = maxSize.height + width = (image.size.width / image.size.height) * height + } + + let newSize = CGSize(width: width, height: height) + + #if os(OSX) + var imageRect = NSRect(origin: CGPoint.zero, size: image.size) + let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) + + // Create NSImage from the CGImage using the new size + return NSImage(cgImage: imageRef!, size: newSize) + #else + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + image.draw(in: CGRect(origin: CGPoint.zero, size: newSize)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? image + UIGraphicsEndImageContext() + return newImage + #endif + } +} + public enum FileOperationType: CustomStringConvertible { case create (path: String) case copy (source: String, destination: String) diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift index 657f834..2759214 100644 --- a/Sources/LocalHelper.swift +++ b/Sources/LocalHelper.swift @@ -56,7 +56,7 @@ public final class LocalFileObject: FileObject { internal class LocalFolderMonitor { fileprivate let source: DispatchSourceFileSystemObject fileprivate let descriptor: CInt - fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default) + fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default) fileprivate var state: Bool = false fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate var url: URL