512 lines
25 KiB
Swift
512 lines
25 KiB
Swift
//
|
|
// ExtendedLocalFileProvider.swift
|
|
// FileProvider
|
|
//
|
|
// Created by Amir Abbas Mousavian.
|
|
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
|
//
|
|
|
|
#if os(macOS) || os(iOS) || os(tvOS)
|
|
import Foundation
|
|
import ImageIO
|
|
import CoreGraphics
|
|
import AVFoundation
|
|
|
|
extension LocalFileProvider: ExtendedFileProvider {
|
|
open func thumbnailOfFileSupported(path: String) -> Bool {
|
|
switch path.pathExtension.lowercased() {
|
|
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
|
return true
|
|
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
|
return true
|
|
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
|
return true
|
|
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
|
return true
|
|
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
|
return true
|
|
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
open func propertiesOfFileSupported(path: String) -> Bool {
|
|
let fileExt = path.pathExtension.lowercased()
|
|
switch fileExt {
|
|
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.imageProperties != nil
|
|
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.audioProperties != nil
|
|
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.videoProperties != nil
|
|
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.pdfProperties != nil
|
|
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.archiveProperties != nil
|
|
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.officeProperties != nil
|
|
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
|
return LocalFileInformationGenerator.customProperties != nil
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? {
|
|
let dimension = dimension ?? CGSize(width: 64, height: 64)
|
|
(dispatch_queue).async {
|
|
var thumbnailImage: ImageClass? = nil
|
|
// Check cache
|
|
let fileURL = self.url(of: path)
|
|
// Create Thumbnail and cache
|
|
switch fileURL.pathExtension.lowercased() {
|
|
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL, dimension)
|
|
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL, dimension)
|
|
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL, dimension)
|
|
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL, dimension)
|
|
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL, dimension)
|
|
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
|
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL, dimension)
|
|
default:
|
|
completionHandler(nil, nil)
|
|
return
|
|
}
|
|
|
|
completionHandler(thumbnailImage, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@discardableResult
|
|
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? {
|
|
(dispatch_queue).async {
|
|
let fileExt = path.pathExtension.lowercased()
|
|
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
|
|
switch fileExt {
|
|
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.imageProperties
|
|
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.audioProperties
|
|
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.videoProperties
|
|
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.pdfProperties
|
|
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.archiveProperties
|
|
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.officeProperties
|
|
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
|
getter = LocalFileInformationGenerator.customProperties
|
|
default:
|
|
break
|
|
}
|
|
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
if let getterMethod = getter {
|
|
(dic, keys) = getterMethod(self.url(of: path))
|
|
}
|
|
|
|
completionHandler(dic, keys, nil)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
|
public struct LocalFileInformationGenerator {
|
|
/// Image extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
|
static public var imageThumbnailExtensions: [String] = ["heic", "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
|
|
|
/// Audio and music extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
|
static public var audioThumbnailExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
|
|
|
/// Video extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
|
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
|
|
|
/// Portable document file extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `["pdf"]`
|
|
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
|
|
|
/// Office document extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `empty`
|
|
static public var officeThumbnailExtensions: [String] = []
|
|
|
|
/// Custom document extensions supportes for thumbnail.
|
|
///
|
|
/// Default: `empty`
|
|
static public var customThumbnailExtensions: [String] = []
|
|
|
|
|
|
/// Image extensions supportes for properties.
|
|
///
|
|
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
|
|
static public var imagePropertiesExtensions: [String] = ["heic", "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
|
|
|
/// Audio and music extensions supportes for properties.
|
|
///
|
|
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
|
static public var audioPropertiesExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
|
|
|
/// Video extensions supportes for properties.
|
|
///
|
|
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
|
static public var videoPropertiesExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
|
|
|
/// Portable document file extensions supportes for properties.
|
|
///
|
|
/// Default: `["pdf"]`
|
|
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
|
|
|
/// Archive extensions (like zip) supportes for properties.
|
|
///
|
|
/// Default: `empty`
|
|
static public var archivePropertiesExtensions: [String] = []
|
|
|
|
/// Office document extensions supportes for properties.
|
|
///
|
|
/// Default: `empty`
|
|
static public var officePropertiesExtensions: [String] = []
|
|
|
|
/// Custom document extensions supportes for properties.
|
|
///
|
|
/// Default: `empty`
|
|
static public var customPropertiesExtensions: [String] = []
|
|
|
|
/// Thumbnail generator closure for image files.
|
|
static public var imageThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
return LocalFileProvider.scaleDown(fileURL: fileURL, toSize: dimension)
|
|
}
|
|
|
|
/// Thumbnail generator closure for audio and music files.
|
|
static public var audioThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
let playerItem = AVPlayerItem(url: fileURL)
|
|
let metadataList = playerItem.asset.commonMetadata
|
|
let commonKeyArtwork = AVMetadataKey.commonKeyArtwork
|
|
for item in metadataList {
|
|
if item.commonKey == commonKeyArtwork {
|
|
if let data = item.dataValue {
|
|
return LocalFileProvider.scaleDown(data: data, toSize: dimension)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Thumbnail generator closure for video files.
|
|
static public var videoThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
let asset = AVAsset(url: fileURL)
|
|
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
|
assetImgGenerate.maximumSize = dimension ?? .zero
|
|
assetImgGenerate.appliesPreferredTrackTransform = true
|
|
let time = CMTime(value: asset.duration.value / 3, timescale: asset.duration.timescale)
|
|
if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) {
|
|
#if os(macOS)
|
|
return ImageClass(cgImage: cgImage, size: .zero)
|
|
#else
|
|
return ImageClass(cgImage: cgImage)
|
|
#endif
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Thumbnail generator closure for portable document files files.
|
|
static public var pdfThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
return LocalFileProvider.convertToImage(pdfURL: fileURL, maxSize: dimension)
|
|
}
|
|
|
|
/// Thumbnail generator closure for office document files.
|
|
/// - Note: No default implementation is avaiable
|
|
static public var officeThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
return nil
|
|
}
|
|
|
|
/// Thumbnail generator closure for custom type of files.
|
|
/// - Note: No default implementation is avaiable
|
|
static public var customThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
|
return nil
|
|
}
|
|
|
|
/// Properties generator closure for image files.
|
|
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
|
|
func add(key: String, value: Any?) {
|
|
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
guard let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as NSDictionary? else {
|
|
return (dic, keys)
|
|
}
|
|
let tiffDict = properties[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
|
let exifDict = properties[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
|
let gpsDict = properties[kCGImagePropertyGPSDictionary as String] as? NSDictionary ?? [:]
|
|
|
|
if let pixelWidth = properties.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = properties.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
|
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
|
}
|
|
add(key: "DPI", value: properties[kCGImagePropertyDPIWidth as String])
|
|
add(key: "Device maker", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
|
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
|
|
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
|
|
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
|
|
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
|
|
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
|
|
|
|
if let latitude = gpsDict[kCGImagePropertyGPSLatitude as String] as? NSNumber,
|
|
let longitude = gpsDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
|
let altitudeDesc = (gpsDict[kCGImagePropertyGPSAltitude as String] as? NSNumber).map({ " at \($0.format(precision: 0))m" }) ?? ""
|
|
add(key: "Location", value: "\(latitude.format()), \(longitude.format())\(altitudeDesc)")
|
|
}
|
|
add(key: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String])
|
|
|
|
add(key: "Color space", value: properties[kCGImagePropertyColorModel as String])
|
|
add(key: "Color depth", value: (properties[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" }))
|
|
add(key: "Color profile", value: properties[kCGImagePropertyProfileName as String])
|
|
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
|
add(key: "White banance", value: exifDict[kCGImagePropertyExifWhiteBalance as String])
|
|
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
|
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
|
|
|
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
|
let expfrac = simplify(Int64(exp.doubleValue * 1_163_962_800_000), 1_163_962_800_000)
|
|
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
|
}
|
|
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? [NSNumber])?.first)
|
|
return (dic, keys)
|
|
}
|
|
|
|
/// Properties generator closure for audio and music files.
|
|
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
|
|
func add(key: String, value: Any?) {
|
|
if let value = value {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
|
|
func makeKeyDescription(_ key: String?) -> String? {
|
|
guard let key = key else {
|
|
return nil
|
|
}
|
|
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: []) else {
|
|
return nil
|
|
}
|
|
let newKey = regex.stringByReplacingMatches(in: key, options: [], range: NSRange(location: 0, length: (key as NSString).length) , withTemplate: "$1 $2")
|
|
return newKey.capitalized
|
|
}
|
|
|
|
func parseLocationData(_ value: String) -> (latitude: Double, longitude: Double, height: Double?)? {
|
|
let scanner = Scanner.init(string: value)
|
|
var latitude: Double = 0.0, longitude: Double = 0.0, height: Double = 0
|
|
|
|
if scanner.scanDouble(&latitude), scanner.scanDouble(&longitude) {
|
|
scanner.scanDouble(&height)
|
|
return (latitude, longitude, height)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
guard fileURL.fileExists else {
|
|
return (dic, keys)
|
|
}
|
|
let playerItem = AVPlayerItem(url: fileURL)
|
|
let metadataList = playerItem.asset.commonMetadata
|
|
for item in metadataList {
|
|
let commonKey = item.commonKey?.rawValue
|
|
if let key = makeKeyDescription(commonKey) {
|
|
if commonKey == "location", let value = item.stringValue, let loc = parseLocationData(value) {
|
|
keys.append(key)
|
|
let heightStr: String = (loc.height as NSNumber?).map({ ", \($0.format(precision: 0))m" }) ?? ""
|
|
dic[key] = "\((loc.latitude as NSNumber).format())°, \((loc.longitude as NSNumber).format())°\(heightStr)"
|
|
} else if let value = item.dateValue {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
} else if let value = item.numberValue {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
} else if let value = item.stringValue {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
}
|
|
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
|
add(key: "Duration", value: ap.duration.formatshort)
|
|
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
|
}
|
|
return (dic, keys)
|
|
}
|
|
|
|
/// Properties generator closure for video files.
|
|
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
|
|
func add(key: String, value: Any?) {
|
|
if let value = value {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
|
|
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
|
dic = audioprops.prop
|
|
keys = audioprops.keys
|
|
dic.removeValue(forKey: "Duration")
|
|
if let index = keys.firstIndex(of: "Duration") {
|
|
keys.remove(at: index)
|
|
}
|
|
}
|
|
let asset = AVURLAsset(url: fileURL, options: nil)
|
|
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
|
|
if let videoTrack = videoTracks.first {
|
|
var bitrate: Float = 0
|
|
let width = Int(videoTrack.naturalSize.width)
|
|
let height = Int(videoTrack.naturalSize.height)
|
|
add(key: "Dimensions", value: "\(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
|
|
}
|
|
add(key: "Duration", value: TimeInterval(duration).formatshort)
|
|
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
|
}
|
|
let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
|
|
// dic["Audio channels"] = audioTracks.count
|
|
var bitrate: Float = 0
|
|
for track in audioTracks {
|
|
bitrate += track.estimatedDataRate
|
|
}
|
|
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
|
return (dic, keys)
|
|
}
|
|
|
|
/// Properties generator closure for protable documents files.
|
|
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
|
|
func add(key: String, value: Any?) {
|
|
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
|
|
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
|
var cfStrValue: CGPDFStringRef?
|
|
if (CGPDFDictionaryGetString(dict, key, &cfStrValue)), let value = cfStrValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
|
return value as String
|
|
}
|
|
var cfArrayValue: CGPDFArrayRef?
|
|
if (CGPDFDictionaryGetArray(dict, key, &cfArrayValue)), let cfArray = cfArrayValue {
|
|
var array = [String]()
|
|
for i in 0..<CGPDFArrayGetCount(cfArray) {
|
|
var cfItemValue: CGPDFStringRef?
|
|
if CGPDFArrayGetString(cfArray, i, &cfItemValue), let item = cfItemValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
|
array.append(item as String)
|
|
}
|
|
}
|
|
return array.joined(separator: ", ")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertDate(_ date: String?) -> Date? {
|
|
guard let date = date else { return nil }
|
|
let dateStr = date.replacingOccurrences(of: "'", with: "").replacingOccurrences(of: "D:", with: "", options: .anchored)
|
|
let dateFormatter = DateFormatter()
|
|
let formats: [String] = ["yyyyMMddHHmmssTZ", "yyyyMMddHHmmssZZZZZ", "yyyyMMddHHmmssZ", "yyyyMMddHHmmss"]
|
|
for format in formats {
|
|
dateFormatter.dateFormat = format
|
|
if let result = dateFormatter.date(from: dateStr) {
|
|
return result
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
guard let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info else {
|
|
return (dic, keys)
|
|
}
|
|
add(key: "Title", value: getKey("Title", from: dict))
|
|
add(key: "Author", value: getKey("Author", from: dict))
|
|
add(key: "Subject", value: getKey("Subject", from: dict))
|
|
add(key: "Producer", value: getKey("Producer", from: dict))
|
|
add(key: "Keywords", value: getKey("Keywords", from: dict))
|
|
var majorVersion: Int32 = 0
|
|
var minorVersion: Int32 = 0
|
|
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
|
if majorVersion > 0 {
|
|
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
|
}
|
|
add(key: "Pages", value: reference.numberOfPages)
|
|
|
|
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
|
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
|
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
|
}
|
|
add(key: "Content creator", value: getKey("Creator", from: dict))
|
|
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
|
|
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
|
|
add(key: "Security", value: reference.isEncrypted ? (reference.isUnlocked ? "Present" : "Password Protected") : "None")
|
|
add(key: "Allows printing", value: reference.allowsPrinting)
|
|
add(key: "Allows copying", value: reference.allowsCopying)
|
|
return (dic, keys)
|
|
}
|
|
|
|
/// Properties generator closure for video files.
|
|
/// - Note: No default implementation is avaiable
|
|
static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
|
|
|
/// Properties generator closure for office doument files.
|
|
/// - Note: No default implementation is avaiable
|
|
static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
|
|
|
/// Properties generator closure for custom type of files.
|
|
/// - Note: No default implementation is avaiable
|
|
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
|
}
|
|
#endif
|