wenweiwei 3 vuotta sitten
vanhempi
commit
21b7579415

+ 677 - 0
BFCommonKit/Classes/BFUtility/PQPHAssetVideoParaseUtil.swift

@@ -0,0 +1,677 @@
+//
+//  PQPHAssetVideoParaseUtil.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/3.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import CoreServices
+import Photos
+import UIKit
+
+var currentExportSession: AVAssetExportSession?
+open class PQPHAssetVideoParaseUtil: NSObject {
+    static var imagesOptions: PHImageRequestOptions = {
+        let imagesOptions = PHImageRequestOptions()
+        imagesOptions.isSynchronous = false
+        imagesOptions.deliveryMode = .fastFormat
+        imagesOptions.resizeMode = .fast
+        imagesOptions.version = .current
+        return imagesOptions
+    }()
+
+    static var singleImageOptions: PHImageRequestOptions = {
+        let singleImageOptions = PHImageRequestOptions()
+        singleImageOptions.isSynchronous = true
+        singleImageOptions.isNetworkAccessAllowed = true
+        singleImageOptions.deliveryMode = .highQualityFormat
+        singleImageOptions.resizeMode = .none
+        singleImageOptions.version = .current
+        return singleImageOptions
+    }()
+
+    static var videoRequestOptions: PHVideoRequestOptions = {
+        let videoRequestOptions = PHVideoRequestOptions()
+        // 解决慢动作视频返回AVComposition而不是AVURLAsset
+//        videoRequestOptions.version = .original
+        videoRequestOptions.version = .current
+        // 下载iCloud视频
+        videoRequestOptions.isNetworkAccessAllowed = true
+        videoRequestOptions.deliveryMode = .mediumQualityFormat
+        return videoRequestOptions
+    }()
+
+    /// PHAsset解析为AVPlayerItem
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasToAVPlayerItem(phAsset: PHAsset, isHighQuality: Bool = false, resultHandler: @escaping (AVPlayerItem?, Float64, [AnyHashable: Any]?) -> Void) {
+        PHImageManager().requestPlayerItem(forVideo: phAsset, options: videoRequestOptions) { playerItem, info in
+            if isHighQuality, (playerItem?.asset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
+                let tempVideoOptions = PHVideoRequestOptions()
+                tempVideoOptions.version = .original
+                // 下载iCloud视频
+                tempVideoOptions.isNetworkAccessAllowed = true
+                tempVideoOptions.deliveryMode = .highQualityFormat
+                tempVideoOptions.progressHandler = { progress, error, pointer, info in
+                    BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                }
+                PHImageManager().requestPlayerItem(forVideo: phAsset, options: tempVideoOptions) { playerItem, info in
+                    let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                    BFLog(message: "size = \(String(describing: size))")
+                    resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
+                }
+            } else {
+                let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                BFLog(message: "size = \(String(describing: size))")
+                resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
+            }
+        }
+    }
+
+    /// PHAsset解析为AVAsset
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasToAVAsset(phAsset: PHAsset, isHighQuality: Bool = true, resultHandler: @escaping (AVAsset?, Int, AVAudioMix?, [AnyHashable: Any]?) -> Void) {
+        PHImageManager.default().requestAVAsset(forVideo: phAsset, options: videoRequestOptions) { avAsset, audioMix, info in
+            if isHighQuality, (avAsset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
+                let tempVideoOptions = PHVideoRequestOptions()
+                tempVideoOptions.version = .current
+                // 下载iCloud视频
+                tempVideoOptions.isNetworkAccessAllowed = true
+                tempVideoOptions.deliveryMode = .highQualityFormat
+                tempVideoOptions.progressHandler = { progress, error, pointer, info in
+                    BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                }
+                PHImageManager.default().requestAVAsset(forVideo: phAsset, options: tempVideoOptions) { tempAvAsset, tempAudioMix, tempInfo in
+                    let size = try! (tempAvAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                    BFLog(message: "size = \(String(describing: size))")
+                    resultHandler(tempAvAsset, size?.fileSize ?? 0, tempAudioMix, tempInfo)
+                }
+            } else {
+                let size = try! (avAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                resultHandler(avAsset, size?.fileSize ?? 0, audioMix, info)
+                BFLog(message: "size = \(String(describing: size))")
+            }
+        }
+    }
+
+    /// PHAsset 转码为.mp4保存本地
+    /// - Parameters:
+    ///   - phAsset: <#phAsset description#>
+    ///   - isAdjustRotationAngle: 是否调整旋转角度
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func exportPHAssetToMP4(phAsset: PHAsset, isAdjustRotationAngle: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
+        if isCancelCurrentExport {
+            currentExportSession?.cancelExport()
+        }
+        PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
+            if avAsset is AVURLAsset {
+                // 创建目录
+                createDirectory(path: photoLibraryDirectory)
+                let fileName = (avAsset as! AVURLAsset).url.absoluteString
+                let filePath = photoLibraryDirectory + fileName.md5.md5 + ".mp4"
+                let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                    BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, filePath, nil)
+                    }
+                } else {
+//                    let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
+                    let tempExportSession: AVAssetExportSession? = nil
+                    if tempExportSession != nil {
+                        BFLog(message: "导出相册视频-正在导出")
+                        return
+                    }
+                    BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
+                    // 删除以创建地址
+                    if FileManager.default.fileExists(atPath: filePath) {
+                        do {
+                            try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+                        } catch {
+                            BFLog(message: "导出相册视频-error == \(error)")
+                        }
+                    }
+                    let requestOptions = PHVideoRequestOptions()
+                    // 解决慢动作视频返回AVComposition而不是AVURLAsset
+                    //        videoRequestOptions.version = .original
+                    requestOptions.version = .current
+                    // 下载iCloud视频
+                    requestOptions.isNetworkAccessAllowed = false
+                    requestOptions.progressHandler = { progress, error, pointer, info in
+                        BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                    }
+                    requestOptions.deliveryMode = deliveryMode ?? .automatic
+                    PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPresetMediumQuality : (deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality), resultHandler: { avAssetExportSession, _ in
+                        BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(String(describing: avAssetExportSession))")
+                        currentExportSession = avAssetExportSession
+                        if avAssetExportSession != nil {
+//                            PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession!
+                        }
+                        avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
+                        avAssetExportSession?.shouldOptimizeForNetworkUse = true
+                        avAssetExportSession?.outputFileType = .mp4
+                        if isAdjustRotationAngle {
+                            let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!)
+                            // mdf by ak 统一导出的视频为30FPS
+                            var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
+                            var mixedTransform: CGAffineTransform = CGAffineTransform()
+                            let videoComposition = AVMutableVideoComposition()
+                            videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+                            let tracks = avAsset?.tracks(withMediaType: .video)
+                            let firstTrack = tracks?.first
+
+                            videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+
+                            mixedTransform = centerTranslate.rotated(by: 0)
+
+                            if rotationAngle == 90 {
+                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi / 2)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                            } else if rotationAngle == 180 {
+                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+                            } else if rotationAngle == 270 {
+                                centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                            }
+                            let roateInstruction = AVMutableVideoCompositionInstruction()
+                            roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero)
+                            if firstTrack != nil {
+                                let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
+                                layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
+                                roateInstruction.layerInstructions = [layRoateInstruction]
+                                videoComposition.instructions = [roateInstruction]
+                                avAssetExportSession?.videoComposition = videoComposition
+                            } else {
+                                BFLog(message: "firstTrack is error !!!")
+                            }
+                        }
+                        avAssetExportSession?.exportAsynchronously(completionHandler: {
+                            BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))")
+                            switch avAssetExportSession?.status {
+                            case .unknown:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            case .waiting:
+                                BFLog(message: "导出相册视频-等待导出mp4:\(filePath)")
+                            case .exporting:
+                                BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)")
+                            case .completed:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, filePath, nil)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-导出完成:\(filePath)")
+                            case .failed:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            case .cancelled:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            default:
+                                break
+                            }
+                        })
+                    })
+                }
+            } else if avAsset is AVComposition {
+                BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
+                let assetResources = PHAssetResource.assetResources(for: phAsset)
+                var resource: PHAssetResource?
+                for assetRes in assetResources {
+                    if assetRes.type == .video || assetRes.type == .pairedVideo {
+                        resource = assetRes
+                    }
+                }
+                if phAsset.mediaType == .video, resource != nil {
+                    let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
+                    let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                    let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                    if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                        DispatchQueue.main.async {
+                            resultHandler(phAsset, avAsset, filePath, nil)
+                        }
+                    } else {
+                        PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
+                            DispatchQueue.main.async {
+                                resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, nil, nil)
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    resultHandler(phAsset, avAsset, nil, nil)
+                }
+            }
+        }
+    }
+
+    /// PHAsset 转码为.mp4保存本地
+    /// - Parameters:
+    ///   - phAsset: <#phAsset description#>
+    ///   - isAdjustRotationAngle: 是否调整旋转角度
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func writePHAssetDataToMP4(phAsset: PHAsset, isAdjustRotationAngle _: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode _: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
+        if isCancelCurrentExport {
+            currentExportSession?.cancelExport()
+        }
+        PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
+            if avAsset is AVURLAsset {
+                // 创建目录
+                createDirectory(path: photoLibraryDirectory)
+                let fileName = (avAsset as! AVURLAsset).url.absoluteString
+                let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                    BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, filePath, nil)
+                    }
+                } else {
+//                    let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
+                    let tempExportSession: AVAssetExportSession? = nil
+                    if tempExportSession != nil {
+                        BFLog(message: "导出相册视频-正在导出")
+                        return
+                    }
+                    BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
+                    // 删除以创建地址
+                    if FileManager.default.fileExists(atPath: filePath) {
+                        do {
+                            try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+                        } catch {
+                            BFLog(message: "导出相册视频-error == \(error)")
+                        }
+                    }
+                    do {
+                        try FileManager.default.copyItem(at: (avAsset as! AVURLAsset).url, to: URL(fileURLWithPath: filePath))
+                    } catch {
+                        BFLog(message: "导出相册视频-error == \(error)")
+                    }
+                }
+            } else if avAsset is AVComposition {
+                BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
+                let assetResources = PHAssetResource.assetResources(for: phAsset)
+                var resource: PHAssetResource?
+                for assetRes in assetResources {
+                    if assetRes.type == .video || assetRes.type == .pairedVideo {
+                        resource = assetRes
+                    }
+                }
+                if phAsset.mediaType == .video, resource != nil {
+                    let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
+                    let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                    let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                    if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                        DispatchQueue.main.async {
+                            resultHandler(phAsset, avAsset, filePath, nil)
+                        }
+                    } else {
+                        PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
+                            DispatchQueue.main.async {
+                                resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, nil, nil)
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    resultHandler(phAsset, avAsset, nil, nil)
+                }
+            }
+        }
+    }
+
+    /// 导出相册视频
+    /// - Parameters:
+    ///   - aVAsset: <#aVAsset description#>
+    ///   - isAdjustRotationAngle: <#isAdjustRotationAngle description#>
+    ///   - resultHandler: <#resultHandler description#>
+    public class func exportAVAssetToMP4(aVAsset: AVURLAsset, isAdjustRotationAngle: Bool = true, resultHandler: @escaping (_ aVAsset: AVURLAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        currentExportSession?.cancelExport()
+        BFLog(message: "开始导出相册视频:url = \(aVAsset.url.absoluteString)")
+        // 创建目录
+        createDirectory(path: photoLibraryDirectory)
+        let fileName = aVAsset.url.absoluteString
+        let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+        let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+        let fileSize = try! aVAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
+        if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+            DispatchQueue.main.async {
+                resultHandler(aVAsset, filePath, nil)
+            }
+        } else {
+            BFLog(message: "未导出视频过,开始导出:aVAsset = \(aVAsset)")
+            // 删除以创建地址
+            try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+            let avAssetExportSession = AVAssetExportSession(asset: aVAsset, presetName: AVAssetExportPreset1280x720)
+            currentExportSession = avAssetExportSession
+            avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
+            avAssetExportSession?.shouldOptimizeForNetworkUse = false
+            avAssetExportSession?.outputFileType = .mp4
+            if isAdjustRotationAngle {
+                let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: aVAsset)
+                // mdf by ak 统一导出的视频为30FPS
+                var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
+                var mixedTransform: CGAffineTransform = CGAffineTransform()
+                let videoComposition = AVMutableVideoComposition()
+                videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+                let tracks = aVAsset.tracks(withMediaType: .video)
+                let firstTrack = tracks.first
+                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+
+                mixedTransform = centerTranslate.rotated(by: 0)
+
+                if rotationAngle == 90 {
+                    centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi / 2)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                } else if rotationAngle == 180 {
+                    centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+                } else if rotationAngle == 270 {
+                    centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                }
+                let roateInstruction = AVMutableVideoCompositionInstruction()
+                roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: aVAsset.duration)
+                let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
+                layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
+                roateInstruction.layerInstructions = [layRoateInstruction]
+                videoComposition.instructions = [roateInstruction]
+                avAssetExportSession?.videoComposition = videoComposition
+            }
+
+            avAssetExportSession?.shouldOptimizeForNetworkUse = true
+            avAssetExportSession?.exportAsynchronously(completionHandler: {
+                BFLog(message: "导出相册视频progress = \(avAssetExportSession?.progress ?? 0)")
+                switch avAssetExportSession?.status {
+                case .unknown:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "导出相册视频发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                case .waiting:
+                    BFLog(message: "等待导出mp4:\(filePath)")
+                case .exporting:
+                    BFLog(message: "导出相册视频中...:\(filePath)")
+                case .completed:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, filePath, nil)
+                    }
+                    BFLog(message: "导出相册视频完成:\(filePath)")
+                case .failed:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "导出相册视频失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                case .cancelled:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "取消导出相册视频:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                default:
+                    break
+                }
+            })
+        }
+    }
+
+    /// 获取视频资源的旋转角度
+    /// - Parameter assert: <#assert description#>
+    /// - Returns: <#description#>
+    public class func videoRotationAngle(assert: AVAsset) -> Int {
+        var rotationAngle: Int = 0
+
+        let tracks = assert.tracks(withMediaType: .video)
+        if tracks.count > 0 {
+            let firstTrack = tracks.first
+            let transform = firstTrack?.preferredTransform
+            if transform?.a == 0, transform?.b == 1.0, transform?.c == -1.0, transform?.d == 0 {
+                rotationAngle = 90
+            } else if transform?.a == -1.0, transform?.b == 0, transform?.c == 0, transform?.d == -1.0 {
+                rotationAngle = 180
+            } else if transform?.a == 0, transform?.b == -1.0, transform?.c == 1.0, transform?.d == 0 {
+                rotationAngle = 270
+            } else if transform?.a == 1.0, transform?.b == 0, transform?.c == 0, transform?.d == 1.0 {
+                rotationAngle = 0
+            }
+        }
+        return rotationAngle
+    }
+
+    /// 裁剪背景音乐并导出
+    /// - Parameters:
+    ///   - url: 原始地址
+    ///   - startTime: 开始时间
+    ///   - endTime: 结束时间
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func cutAudioToLocal(url: String, startTime: Float, endTime: Float, resultHandler: @escaping (_ url: String, _ filePath: String?, _ startTime: Float, _ endTime: Float, _ errorMsg: String?) -> Void) {
+        // 创建目录
+        createDirectory(path: bgMusicDirectory)
+        let filePath = bgMusicDirectory + url.md5 + ".mp3"
+        let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+        if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 {
+            DispatchQueue.main.async {
+                resultHandler(url, filePath, startTime, endTime, nil)
+            }
+        } else {
+            // 删除以创建地址
+            try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+            let audioAsset = AVURLAsset(url: URL(string: url)!)
+            audioAsset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
+                let status = audioAsset.statusOfValue(forKey: "tracks", error: nil)
+                switch status {
+                case .loaded: // 加载完成
+                    // AVAssetExportPresetPassthrough /AVAssetExportPresetAppleM4A
+                    let exportSession = AVAssetExportSession(asset: audioAsset, presetName: AVAssetExportPresetHighestQuality)
+                    exportSession?.outputURL = URL(fileURLWithPath: filePath)
+                    exportSession?.outputFileType = .mp3
+                    exportSession?.timeRange = CMTimeRange(start: CMTime(seconds: Double(startTime), preferredTimescale: 1000), end: CMTime(seconds: Double(endTime), preferredTimescale: 1000))
+                    exportSession?.exportAsynchronously(completionHandler: {
+                        switch exportSession?.status {
+                        case .waiting:
+                            BFLog(message: "等待导出mp3:\(filePath)")
+                        case .exporting:
+                            BFLog(message: "导出中...:\(filePath)")
+                        case .completed:
+                            DispatchQueue.main.async {
+                                resultHandler(url, filePath, startTime, endTime, nil)
+                            }
+                            BFLog(message: "导出完成:\(filePath)")
+
+                        case .cancelled, .failed, .unknown:
+                            DispatchQueue.main.async {
+                                resultHandler(url, nil, startTime, endTime, exportSession?.error?.localizedDescription)
+                            }
+                            BFLog(message: "导出失败:\(filePath),\(exportSession?.error?.localizedDescription ?? "")")
+                        default:
+                            break
+                        }
+                    })
+                case .loading:
+                    BFLog(message: "加载中...:\(url)")
+                case .failed, .cancelled, .unknown:
+                    DispatchQueue.main.async {
+                        resultHandler(url, nil, startTime, endTime, "导出失败")
+                    }
+                default:
+                    break
+                }
+            }
+        }
+    }
+
+    /// 创建本地保存地址
+    /// - Parameters:
+    ///   - sourceFilePath: <#sourceFilePath description#>
+    ///   - completeHandle: <#completeHandle description#>
+    /// - Returns: <#description#>
+    public class func createLocalFile(sourceFilePath: String, completeHandle: (_ isFileExists: Bool, _ isCreateSuccess: Bool, _ filePath: String) -> Void) {
+        let cLocalPath = NSString(string: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("\(sourceFilePath.md5).mp4")
+        if FileManager.default.fileExists(atPath: cLocalPath) {
+            BFLog(message: "文件已经存在:\(cLocalPath)")
+            completeHandle(true, false, cLocalPath)
+        } else {
+            let result = FileManager.default.createFile(atPath: cLocalPath, contents: nil, attributes: nil)
+            BFLog(message: "文件创建:\(cLocalPath),\(result)")
+            completeHandle(false, result, cLocalPath)
+        }
+    }
+
+    /// 获取图库图片
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - itemSize: <#itemSize description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func requestAssetImage(asset: PHAsset, itemSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
+        PHCachingImageManager().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: imagesOptions, resultHandler: { image, info in
+            BFLog(message: "info = \(info ?? [:])")
+            if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0" {
+                resultHandler(image, info)
+            }
+        })
+    }
+
+    /// 获取图库原图
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func requestAssetOringinImage(asset: PHAsset, resultHandler: @escaping (_ isGIF: Bool, _ data: Data?, UIImage?, [AnyHashable: Any]?) -> Void) {
+        PHCachingImageManager().requestImageData(for: asset, options: singleImageOptions) { data, _, _, info in
+            var image: UIImage?
+            if data != nil {
+                image = UIImage(data: data!)
+            }
+            if info?.keys.contains("PHImageFileUTIKey") ?? false, "\(info?["PHImageFileUTIKey"] ?? "")" == "com.compuserve.gif" {
+                resultHandler(true, data, image, info)
+            } else {
+                resultHandler(false, data, image, info)
+            }
+        }
+    }
+
+    /// 获取gif帧跟时长
+    /// - Parameters:
+    ///   - data: <#data description#>
+    ///   - isRenderingTemplate
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasGIFImage(data: Data, isRenderingColor: UIColor? = nil, resultHandler: @escaping (_ data: Data, _ images: [UIImage]?, _ duration: Double?) -> Void) {
+        let info: [String: Any] = [
+            kCGImageSourceShouldCache as String: true,
+            kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF,
+        ]
+        guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
+            resultHandler(data, nil, nil)
+            BFLog(message: "获取gifimageSource 失败")
+            return
+        }
+        // 获取帧数
+        let frameCount = CGImageSourceGetCount(imageSource)
+        var gifDuration = 0.0
+        var images = [UIImage]()
+        for i in 0 ..< frameCount {
+            // 取出索引对应的图片
+            guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
+                BFLog(message: "取出对应的图片失败")
+                return
+            }
+            if frameCount == 1 {
+                // 单帧
+                gifDuration = .infinity
+            } else {
+                // 1.获取gif没帧的时间间隔
+                // 获取到该帧图片的属性字典
+                guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as? [String: Any] else {
+                    BFLog(message: "取出对应的图片属性失败")
+                    return
+                }
+                // 获取该帧图片中的GIF相关的属性字典
+                guard let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
+                    BFLog(message: "取出对应的图片属性失败")
+                    return
+                }
+                let defaultFrameDuration = 0.1
+                // 获取该帧图片的播放时间
+                let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
+                // 如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
+                let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
+                let duration = unclampedDelayTime ?? delayTime
+                guard let frameDuration = duration else {
+                    BFLog(message: "获取帧时间间隔失败")
+                    return
+                }
+                // 对于播放时间低于0.011s的,重新指定时长为0.100s;
+                let gifFrameDuration = frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
+                // 计算总时间
+                gifDuration += gifFrameDuration
+                // 2.图片
+                var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
+                if isRenderingColor != nil && frameImage != nil {
+                    frameImage = tintImage(image: frameImage!, color: isRenderingColor!, blendMode: .destinationIn)
+                }
+                if frameImage != nil {
+                    images.append(frameImage!)
+                }
+            }
+        }
+        resultHandler(data, images, gifDuration)
+    }
+    
+    /// 改变图片主题颜色
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - blendMode: <#blendMode description#>
+    /// - Returns: <#description#>
+    public class func tintImage(image:UIImage,color: UIColor, blendMode: CGBlendMode) -> UIImage? {
+        let rect = CGRect(origin: CGPoint.zero, size: image.size)
+        UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
+        color.setFill()
+        UIRectFill(rect)
+        image.draw(in: rect, blendMode: blendMode, alpha: 1.0)
+        let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return tintedImage
+    }
+}

+ 18 - 0
BFCommonKit/Classes/Vendors/DES/DES3Util.h

@@ -0,0 +1,18 @@
+//
+//  DES3Util.h
+//  DES
+//
+//  Created by Toni on 12-12-27.
+//  Copyright (c) 2012年 sinofreely. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "GTMBase64.h"
+#import <CommonCrypto/CommonCryptor.h>
+@interface DES3Util : NSObject
+
+// 加密方法
++ (NSString *)encryptUseDES:(NSString *)plainText key:(NSString *)key;
+// 解密方法
++ (NSString *)decryptUseDES:(NSString *)cipherText key:(NSString *)key;
+@end

+ 121 - 0
BFCommonKit/Classes/Vendors/DES/DES3Util.m

@@ -0,0 +1,121 @@
+//
+//  DES3Util.m
+//  DES
+//
+//  Created by Toni on 12-12-27.
+//  Copyright (c) 2012年 sinofreely. All rights reserved.
+//
+
+#import "DES3Util.h"
+
+@implementation DES3Util
+
+
+const Byte iv[] = {1,2,3,4,5,6,7,8};
+
+// Des加密
++ (NSString *) encryptUseDES:(NSString *)plainText key:(NSString *)key
+{
+    // 密文
+    NSString *ciphertext = nil;
+    if (!plainText || ![plainText isKindOfClass:[NSString class]] || !plainText.length) {
+        return ciphertext;
+    }
+    // 进行编码(避免特殊字符导致加密/解密失败)
+    plainText = [self UrlValueEncode:plainText];
+    // 加密后的数据
+    uint8_t *dataOut = NULL;
+    size_t dataOutAvailable = 0;
+    size_t dataOutMove = 0;
+    
+    dataOutAvailable = (plainText.length + kCCBlockSizeDES) & ~(kCCBlockSizeDES - 1);
+    dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
+    // 将已开辟内存空间buffer的首1个字节的值设为0
+    memset((void *)dataOut, 0x0, dataOutAvailable);
+    
+    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, // 操作名称:加密
+                                          kCCAlgorithmDES, // 加密算法
+                                          kCCOptionPKCS7Padding | kCCOptionECBMode, // 填充模式,ECB模式
+                                          key.UTF8String, // 加密秘钥
+                                          kCCKeySizeDES, // 秘钥大小,和加密算法一致
+                                          NULL, // 初始向量:ECB模式为空
+                                          plainText.UTF8String, // 加密的明文
+                                          (size_t)plainText.length, // 加密明文的大小
+                                          dataOut, // 密文的接受者
+                                          dataOutAvailable, // 预计密文的大小
+                                          &dataOutMove); // 加密后密文的实际大小
+    if (cryptStatus == kCCSuccess) {
+        NSData *data = [NSData dataWithBytes:dataOut length:(NSUInteger)dataOutMove];
+        ciphertext = [GTMBase64 stringByEncodingData:data];
+    }
+    return ciphertext;
+}
+
+
+
+// Des解密
++ (NSString *)decryptUseDES:(NSString *)cipherText key:(NSString *)key
+{
+    if (!cipherText || ![cipherText isKindOfClass:[NSString class]] || !cipherText.length) {
+        return nil;
+    }
+    // 将16进制转为data
+    NSData *cipherData = [GTMBase64 decodeString:cipherText];
+    // 解密后的数据
+    uint8_t *dataOut = NULL;
+    size_t dataOutAvailable = 0;
+    size_t dataOutMove = 0;
+    
+    dataOutAvailable = (cipherData.length + kCCBlockSizeDES) & ~(kCCBlockSizeDES - 1);
+    dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
+    // 将已开辟内存空间buffer的首1个字节的值设为0
+    memset((void *)dataOut, 0x0, dataOutAvailable);
+    
+    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
+                                          kCCAlgorithmDES,
+                                          kCCOptionPKCS7Padding | kCCOptionECBMode,
+                                          key.UTF8String,
+                                          kCCKeySizeDES,
+                                          NULL,
+                                          cipherData.bytes,
+                                          (size_t)cipherData.length,
+                                          dataOut,
+                                          dataOutAvailable,
+                                          &dataOutMove);
+    NSString* plainText = nil;
+    if (cryptStatus == kCCSuccess) {
+        NSData *data = [NSData dataWithBytes:dataOut length:(NSUInteger)dataOutMove];
+        // 进行解码(避免特殊字符导致加密/解密失败)
+        plainText = [self decodeFromPercentEscapeString:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
+    }
+    return plainText;
+}
+
+// url编码
++ (NSString *)UrlValueEncode:(NSString *)str{
+    if (!str || ![str isKindOfClass:[NSString class]] || !str.length) {
+        return nil;
+    }
+    NSString *result = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
+                                                                                             (CFStringRef)str,
+                                                                                             NULL,
+                                                                                             CFSTR("!*'();:@&=+$,/?%#[]"),
+                                                                                             kCFStringEncodingUTF8));
+    
+    return result;
+}
+// url 解码
++ (NSString *)decodeFromPercentEscapeString: (NSString *) input
+{
+    if (!input || ![input isKindOfClass:[NSString class]] || !input.length) {
+        return nil;
+    }
+    NSMutableString *outputStr = [NSMutableString stringWithString:input];
+    [outputStr replaceOccurrencesOfString:@"+"
+                               withString:@" "
+                                  options:NSLiteralSearch
+                                    range:NSMakeRange(0, [outputStr length])];
+    
+    return [outputStr stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+}
+@end

+ 189 - 0
BFCommonKit/Classes/Vendors/DES/GTMBase64.h

@@ -0,0 +1,189 @@
+//
+//  GTMBase64.h
+//
+//  Copyright 2006-2008 Google Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License.  You may obtain a copy
+//  of the License at
+// 
+//  http://www.apache.org/licenses/LICENSE-2.0
+// 
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+//
+
+
+// WARNING: This class provides a subset of the functionality available in
+// GTMStringEncoding and may go away in the future.
+// Please consider using GTMStringEncoding instead.
+
+
+#import <Foundation/Foundation.h>
+#import "GTMDefines.h"
+
+// GTMBase64
+//
+/// Helper for handling Base64 and WebSafeBase64 encodings
+//
+/// The webSafe methods use different character set and also the results aren't
+/// always padded to a multiple of 4 characters.  This is done so the resulting
+/// data can be used in urls and url query arguments without needing any
+/// encoding.  You must use the webSafe* methods together, the data does not
+/// interop with the RFC methods.
+//
+@interface GTMBase64 : NSObject
+
+//
+// Standard Base64 (RFC) handling
+//
+
+// encodeData:
+//
+/// Base64 encodes contents of the NSData object.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)encodeData:(NSData *)data;
+
+// decodeData:
+//
+/// Base64 decodes contents of the NSData object.
+//
+/// Returns:
+///   A new autoreleased NSData with the decoded payload.  nil for any error.
+//
++(NSData *)decodeData:(NSData *)data;
+
+// encodeBytes:length:
+//
+/// Base64 encodes the data pointed at by |bytes|.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length;
+
+// decodeBytes:length:
+//
+/// Base64 decodes the data pointed at by |bytes|.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length;
+
+// stringByEncodingData:
+//
+/// Base64 encodes contents of the NSData object.
+//
+/// Returns:
+///   A new autoreleased NSString with the encoded payload.  nil for any error.
+//
++(NSString *)stringByEncodingData:(NSData *)data;
+
+// stringByEncodingBytes:length:
+//
+/// Base64 encodes the data pointed at by |bytes|.
+//
+/// Returns:
+///   A new autoreleased NSString with the encoded payload.  nil for any error.
+//
++(NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length;
+
+// decodeString:
+//
+/// Base64 decodes contents of the NSString.
+//
+/// Returns:
+///   A new autoreleased NSData with the decoded payload.  nil for any error.
+//
++(NSData *)decodeString:(NSString *)string;
+
+//
+// Modified Base64 encoding so the results can go onto urls.
+//
+// The changes are in the characters generated and also allows the result to
+// not be padded to a multiple of 4.
+// Must use the matching call to encode/decode, won't interop with the
+// RFC versions.
+//
+
+// webSafeEncodeData:padded:
+//
+/// WebSafe Base64 encodes contents of the NSData object.  If |padded| is YES
+/// then padding characters are added so the result length is a multiple of 4.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)webSafeEncodeData:(NSData *)data
+                      padded:(BOOL)padded;
+
+// webSafeDecodeData:
+//
+/// WebSafe Base64 decodes contents of the NSData object.
+//
+/// Returns:
+///   A new autoreleased NSData with the decoded payload.  nil for any error.
+//
++(NSData *)webSafeDecodeData:(NSData *)data;
+
+// webSafeEncodeBytes:length:padded:
+//
+/// WebSafe Base64 encodes the data pointed at by |bytes|.  If |padded| is YES
+/// then padding characters are added so the result length is a multiple of 4.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)webSafeEncodeBytes:(const void *)bytes
+                       length:(NSUInteger)length
+                       padded:(BOOL)padded;
+
+// webSafeDecodeBytes:length:
+//
+/// WebSafe Base64 decodes the data pointed at by |bytes|.
+//
+/// Returns:
+///   A new autoreleased NSData with the encoded payload.  nil for any error.
+//
++(NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length;
+
+// stringByWebSafeEncodingData:padded:
+//
+/// WebSafe Base64 encodes contents of the NSData object.  If |padded| is YES
+/// then padding characters are added so the result length is a multiple of 4.
+//
+/// Returns:
+///   A new autoreleased NSString with the encoded payload.  nil for any error.
+//
++(NSString *)stringByWebSafeEncodingData:(NSData *)data
+                                  padded:(BOOL)padded;
+
+// stringByWebSafeEncodingBytes:length:padded:
+//
+/// WebSafe Base64 encodes the data pointed at by |bytes|.  If |padded| is YES
+/// then padding characters are added so the result length is a multiple of 4.
+//
+/// Returns:
+///   A new autoreleased NSString with the encoded payload.  nil for any error.
+//
++(NSString *)stringByWebSafeEncodingBytes:(const void *)bytes
+                                   length:(NSUInteger)length
+                                   padded:(BOOL)padded;
+
+// webSafeDecodeString:
+//
+/// WebSafe Base64 decodes contents of the NSString.
+//
+/// Returns:
+///   A new autoreleased NSData with the decoded payload.  nil for any error.
+//
++(NSData *)webSafeDecodeString:(NSString *)string;
+
+@end

+ 692 - 0
BFCommonKit/Classes/Vendors/DES/GTMBase64.m

@@ -0,0 +1,692 @@
+//
+//  GTMBase64.m
+//
+//  Copyright 2006-2008 Google Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License.  You may obtain a copy
+//  of the License at
+// 
+//  http://www.apache.org/licenses/LICENSE-2.0
+// 
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+//
+
+#import "GTMBase64.h"
+#import "GTMDefines.h"
+
+static const char *kBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char *kWebSafeBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+static const char kBase64PaddingChar = '=';
+static const char kBase64InvalidChar = 99;
+
+static const char kBase64DecodeChars[] = {
+    // This array was generated by the following code:
+    // #include <sys/time.h>
+    // #include <stdlib.h>
+    // #include <string.h>
+    // main()
+    // {
+    //   static const char Base64[] =
+    //     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    //   char *pos;
+    //   int idx, i, j;
+    //   printf("    ");
+    //   for (i = 0; i < 255; i += 8) {
+    //     for (j = i; j < i + 8; j++) {
+    //       pos = strchr(Base64, j);
+    //       if ((pos == NULL) || (j == 0))
+    //         idx = 99;
+    //       else
+    //         idx = pos - Base64;
+    //       if (idx == 99)
+    //         printf(" %2d,     ", idx);
+    //       else
+    //         printf(" %2d/*%c*/,", idx, j);
+    //     }
+    //     printf("\n    ");
+    //   }
+    // }
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      62/*+*/, 99,      99,      99,      63/*/ */,
+    52/*0*/, 53/*1*/, 54/*2*/, 55/*3*/, 56/*4*/, 57/*5*/, 58/*6*/, 59/*7*/,
+    60/*8*/, 61/*9*/, 99,      99,      99,      99,      99,      99,
+    99,       0/*A*/,  1/*B*/,  2/*C*/,  3/*D*/,  4/*E*/,  5/*F*/,  6/*G*/,
+    7/*H*/,  8/*I*/,  9/*J*/, 10/*K*/, 11/*L*/, 12/*M*/, 13/*N*/, 14/*O*/,
+    15/*P*/, 16/*Q*/, 17/*R*/, 18/*S*/, 19/*T*/, 20/*U*/, 21/*V*/, 22/*W*/,
+    23/*X*/, 24/*Y*/, 25/*Z*/, 99,      99,      99,      99,      99,
+    99,      26/*a*/, 27/*b*/, 28/*c*/, 29/*d*/, 30/*e*/, 31/*f*/, 32/*g*/,
+    33/*h*/, 34/*i*/, 35/*j*/, 36/*k*/, 37/*l*/, 38/*m*/, 39/*n*/, 40/*o*/,
+    41/*p*/, 42/*q*/, 43/*r*/, 44/*s*/, 45/*t*/, 46/*u*/, 47/*v*/, 48/*w*/,
+    49/*x*/, 50/*y*/, 51/*z*/, 99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99
+};
+
+static const char kWebSafeBase64DecodeChars[] = {
+    // This array was generated by the following code:
+    // #include <sys/time.h>
+    // #include <stdlib.h>
+    // #include <string.h>
+    // main()
+    // {
+    //   static const char Base64[] =
+    //     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+    //   char *pos;
+    //   int idx, i, j;
+    //   printf("    ");
+    //   for (i = 0; i < 255; i += 8) {
+    //     for (j = i; j < i + 8; j++) {
+    //       pos = strchr(Base64, j);
+    //       if ((pos == NULL) || (j == 0))
+    //         idx = 99;
+    //       else
+    //         idx = pos - Base64;
+    //       if (idx == 99)
+    //         printf(" %2d,     ", idx);
+    //       else
+    //         printf(" %2d/*%c*/,", idx, j);
+    //     }
+    //     printf("\n    ");
+    //   }
+    // }
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      62/*-*/, 99,      99,
+    52/*0*/, 53/*1*/, 54/*2*/, 55/*3*/, 56/*4*/, 57/*5*/, 58/*6*/, 59/*7*/,
+    60/*8*/, 61/*9*/, 99,      99,      99,      99,      99,      99,
+    99,       0/*A*/,  1/*B*/,  2/*C*/,  3/*D*/,  4/*E*/,  5/*F*/,  6/*G*/,
+    7/*H*/,  8/*I*/,  9/*J*/, 10/*K*/, 11/*L*/, 12/*M*/, 13/*N*/, 14/*O*/,
+    15/*P*/, 16/*Q*/, 17/*R*/, 18/*S*/, 19/*T*/, 20/*U*/, 21/*V*/, 22/*W*/,
+    23/*X*/, 24/*Y*/, 25/*Z*/, 99,      99,      99,      99,      63/*_*/,
+    99,      26/*a*/, 27/*b*/, 28/*c*/, 29/*d*/, 30/*e*/, 31/*f*/, 32/*g*/,
+    33/*h*/, 34/*i*/, 35/*j*/, 36/*k*/, 37/*l*/, 38/*m*/, 39/*n*/, 40/*o*/,
+    41/*p*/, 42/*q*/, 43/*r*/, 44/*s*/, 45/*t*/, 46/*u*/, 47/*v*/, 48/*w*/,
+    49/*x*/, 50/*y*/, 51/*z*/, 99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99,
+    99,      99,      99,      99,      99,      99,      99,      99
+};
+
+
+// Tests a character to see if it's a whitespace character.
+//
+// Returns:
+//   YES if the character is a whitespace character.
+//   NO if the character is not a whitespace character.
+//
+GTM_INLINE BOOL IsSpace(unsigned char c) {
+    // we use our own mapping here because we don't want anything w/ locale
+    // support.
+    static BOOL kSpaces[256] = {
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 1,  // 0-9
+        1, 1, 1, 1, 0, 0, 0, 0, 0, 0,  // 10-19
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 20-29
+        0, 0, 1, 0, 0, 0, 0, 0, 0, 0,  // 30-39
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 40-49
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 50-59
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 60-69
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 70-79
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 80-89
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 90-99
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 100-109
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 110-119
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 120-129
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 130-139
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 140-149
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 150-159
+        1, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 160-169
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 170-179
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 180-189
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 190-199
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 200-209
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 210-219
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 220-229
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 230-239
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 240-249
+        0, 0, 0, 0, 0, 1,              // 250-255
+    };
+    return kSpaces[c];
+}
+
+// Calculate how long the data will be once it's base64 encoded.
+//
+// Returns:
+//   The guessed encoded length for a source length
+//
+GTM_INLINE NSUInteger CalcEncodedLength(NSUInteger srcLen, BOOL padded) {
+    NSUInteger intermediate_result = 8 * srcLen + 5;
+    NSUInteger len = intermediate_result / 6;
+    if (padded) {
+        len = ((len + 3) / 4) * 4;
+    }
+    return len;
+}
+
+// Tries to calculate how long the data will be once it's base64 decoded.
+// Unlike the above, this is always an upperbound, since the source data
+// could have spaces and might end with the padding characters on them.
+//
+// Returns:
+//   The guessed decoded length for a source length
+//
+GTM_INLINE NSUInteger GuessDecodedLength(NSUInteger srcLen) {
+    return (srcLen + 3) / 4 * 3;
+}
+
+
+@interface GTMBase64 (PrivateMethods)
+
++(NSData *)baseEncode:(const void *)bytes
+               length:(NSUInteger)length
+              charset:(const char *)charset
+               padded:(BOOL)padded;
+
++(NSData *)baseDecode:(const void *)bytes
+               length:(NSUInteger)length
+              charset:(const char*)charset
+       requirePadding:(BOOL)requirePadding;
+
++(NSUInteger)baseEncode:(const char *)srcBytes
+                 srcLen:(NSUInteger)srcLen
+              destBytes:(char *)destBytes
+                destLen:(NSUInteger)destLen
+                charset:(const char *)charset
+                 padded:(BOOL)padded;
+
++(NSUInteger)baseDecode:(const char *)srcBytes
+                 srcLen:(NSUInteger)srcLen
+              destBytes:(char *)destBytes
+                destLen:(NSUInteger)destLen
+                charset:(const char *)charset
+         requirePadding:(BOOL)requirePadding;
+
+@end
+
+
+@implementation GTMBase64
+
+//
+// Standard Base64 (RFC) handling
+//
+
++(NSData *)encodeData:(NSData *)data {
+    return [self baseEncode:[data bytes]
+                     length:[data length]
+                    charset:kBase64EncodeChars
+                     padded:YES];
+}
+
++(NSData *)decodeData:(NSData *)data {
+    return [self baseDecode:[data bytes]
+                     length:[data length]
+                    charset:kBase64DecodeChars
+             requirePadding:YES];
+}
+
++(NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length {
+    return [self baseEncode:bytes
+                     length:length
+                    charset:kBase64EncodeChars
+                     padded:YES];
+}
+
++(NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length {
+    return [self baseDecode:bytes
+                     length:length
+                    charset:kBase64DecodeChars
+             requirePadding:YES];
+}
+
++(NSString *)stringByEncodingData:(NSData *)data {
+    NSString *result = nil;
+    NSData *converted = [self baseEncode:[data bytes]
+                                  length:[data length]
+                                 charset:kBase64EncodeChars
+                                  padded:YES];
+    if (converted) {
+        result = [[NSString alloc] initWithData:converted
+                                        encoding:NSASCIIStringEncoding];
+    }
+    return result;
+}
+
++(NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length {
+    NSString *result = nil;
+    NSData *converted = [self baseEncode:bytes
+                                  length:length
+                                 charset:kBase64EncodeChars
+                                  padded:YES];
+    if (converted) {
+        result = [[NSString alloc] initWithData:converted
+                                        encoding:NSASCIIStringEncoding];
+    }
+    return result;
+}
+
++(NSData *)decodeString:(NSString *)string {
+    NSData *result = nil;
+    NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding];
+    if (data) {
+        result = [self baseDecode:[data bytes]
+                           length:[data length]
+                          charset:kBase64DecodeChars
+                   requirePadding:YES];
+    }
+    return result;
+}
+
+//
+// Modified Base64 encoding so the results can go onto urls.
+//
+// The changes are in the characters generated and also the result isn't
+// padded to a multiple of 4.
+// Must use the matching call to encode/decode, won't interop with the
+// RFC versions.
+//
+
++(NSData *)webSafeEncodeData:(NSData *)data
+                      padded:(BOOL)padded {
+    return [self baseEncode:[data bytes]
+                     length:[data length]
+                    charset:kWebSafeBase64EncodeChars
+                     padded:padded];
+}
+
++(NSData *)webSafeDecodeData:(NSData *)data {
+    return [self baseDecode:[data bytes]
+                     length:[data length]
+                    charset:kWebSafeBase64DecodeChars
+             requirePadding:NO];
+}
+
++(NSData *)webSafeEncodeBytes:(const void *)bytes
+                       length:(NSUInteger)length
+                       padded:(BOOL)padded {
+    return [self baseEncode:bytes
+                     length:length
+                    charset:kWebSafeBase64EncodeChars
+                     padded:padded];
+}
+
++(NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length {
+    return [self baseDecode:bytes
+                     length:length
+                    charset:kWebSafeBase64DecodeChars
+             requirePadding:NO];
+}
+
++(NSString *)stringByWebSafeEncodingData:(NSData *)data
+                                  padded:(BOOL)padded {
+    NSString *result = nil;
+    NSData *converted = [self baseEncode:[data bytes]
+                                  length:[data length]
+                                 charset:kWebSafeBase64EncodeChars
+                                  padded:padded];
+    if (converted) {
+        result = [[NSString alloc] initWithData:converted
+                                        encoding:NSASCIIStringEncoding];
+    }
+    return result;
+}
+
++(NSString *)stringByWebSafeEncodingBytes:(const void *)bytes
+                                   length:(NSUInteger)length
+                                   padded:(BOOL)padded {
+    NSString *result = nil;
+    NSData *converted = [self baseEncode:bytes
+                                  length:length
+                                 charset:kWebSafeBase64EncodeChars
+                                  padded:padded];
+    if (converted) {
+        result = [[NSString alloc] initWithData:converted
+                                        encoding:NSASCIIStringEncoding];
+    }
+    return result;
+}
+
++(NSData *)webSafeDecodeString:(NSString *)string {
+    NSData *result = nil;
+    NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding];
+    if (data) {
+        result = [self baseDecode:[data bytes]
+                           length:[data length]
+                          charset:kWebSafeBase64DecodeChars
+                   requirePadding:NO];
+    }
+    return result;
+}
+
+@end
+
+@implementation GTMBase64 (PrivateMethods)
+
+//
+// baseEncode:length:charset:padded:
+//
+// Does the common lifting of creating the dest NSData.  it creates & sizes the
+// data for the results.  |charset| is the characters to use for the encoding
+// of the data.  |padding| controls if the encoded data should be padded to a
+// multiple of 4.
+//
+// Returns:
+//   an autorelease NSData with the encoded data, nil if any error.
+//
++(NSData *)baseEncode:(const void *)bytes
+               length:(NSUInteger)length
+              charset:(const char *)charset
+               padded:(BOOL)padded {
+    // how big could it be?
+    NSUInteger maxLength = CalcEncodedLength(length, padded);
+    // make space
+    NSMutableData *result = [NSMutableData data];
+    [result setLength:maxLength];
+    // do it
+    NSUInteger finalLength = [self baseEncode:bytes
+                                       srcLen:length
+                                    destBytes:[result mutableBytes]
+                                      destLen:[result length]
+                                      charset:charset
+                                       padded:padded];
+    if (finalLength) {
+        _GTMDevAssert(finalLength == maxLength, @"how did we calc the length wrong?");
+    } else {
+        // shouldn't happen, this means we ran out of space
+        result = nil;
+    }
+    return result;
+}
+
+//
+// baseDecode:length:charset:requirePadding:
+//
+// Does the common lifting of creating the dest NSData.  it creates & sizes the
+// data for the results.  |charset| is the characters to use for the decoding
+// of the data.
+//
+// Returns:
+//   an autorelease NSData with the decoded data, nil if any error.
+//
+//
++(NSData *)baseDecode:(const void *)bytes
+               length:(NSUInteger)length
+              charset:(const char *)charset
+       requirePadding:(BOOL)requirePadding {
+    // could try to calculate what it will end up as
+    NSUInteger maxLength = GuessDecodedLength(length);
+    // make space
+    NSMutableData *result = [NSMutableData data];
+    [result setLength:maxLength];
+    // do it
+    NSUInteger finalLength = [self baseDecode:bytes
+                                       srcLen:length
+                                    destBytes:[result mutableBytes]
+                                      destLen:[result length]
+                                      charset:charset
+                               requirePadding:requirePadding];
+    if (finalLength) {
+        if (finalLength != maxLength) {
+            // resize down to how big it was
+            [result setLength:finalLength];
+        }
+    } else {
+        // either an error in the args, or we ran out of space
+        result = nil;
+    }
+    return result;
+}
+
+//
+// baseEncode:srcLen:destBytes:destLen:charset:padded:
+//
+// Encodes the buffer into the larger.  returns the length of the encoded
+// data, or zero for an error.
+// |charset| is the characters to use for the encoding
+// |padded| tells if the result should be padded to a multiple of 4.
+//
+// Returns:
+//   the length of the encoded data.  zero if any error.
+//
++(NSUInteger)baseEncode:(const char *)srcBytes
+                 srcLen:(NSUInteger)srcLen
+              destBytes:(char *)destBytes
+                destLen:(NSUInteger)destLen
+                charset:(const char *)charset
+                 padded:(BOOL)padded {
+    if (!srcLen || !destLen || !srcBytes || !destBytes) {
+        return 0;
+    }
+    
+    char *curDest = destBytes;
+    const unsigned char *curSrc = (const unsigned char *)(srcBytes);
+    
+    // Three bytes of data encodes to four characters of cyphertext.
+    // So we can pump through three-byte chunks atomically.
+    while (srcLen > 2) {
+        // space?
+        _GTMDevAssert(destLen >= 4, @"our calc for encoded length was wrong");
+        curDest[0] = charset[curSrc[0] >> 2];
+        curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)];
+        curDest[2] = charset[((curSrc[1] & 0x0f) << 2) + (curSrc[2] >> 6)];
+        curDest[3] = charset[curSrc[2] & 0x3f];
+        
+        curDest += 4;
+        curSrc += 3;
+        srcLen -= 3;
+        destLen -= 4;
+    }
+    
+    // now deal with the tail (<=2 bytes)
+    switch (srcLen) {
+        case 0:
+            // Nothing left; nothing more to do.
+            break;
+        case 1:
+            // One byte left: this encodes to two characters, and (optionally)
+            // two pad characters to round out the four-character cypherblock.
+            _GTMDevAssert(destLen >= 2, @"our calc for encoded length was wrong");
+            curDest[0] = charset[curSrc[0] >> 2];
+            curDest[1] = charset[(curSrc[0] & 0x03) << 4];
+            curDest += 2;
+            if (padded) {
+                _GTMDevAssert(destLen >= 4, @"our calc for encoded length was wrong");
+                curDest[0] = kBase64PaddingChar;
+                curDest[1] = kBase64PaddingChar;
+                curDest += 2;
+            }
+            break;
+        case 2:
+            // Two bytes left: this encodes to three characters, and (optionally)
+            // one pad character to round out the four-character cypherblock.
+            _GTMDevAssert(destLen >= 3, @"our calc for encoded length was wrong");
+            curDest[0] = charset[curSrc[0] >> 2];
+            curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)];
+            curDest[2] = charset[(curSrc[1] & 0x0f) << 2];
+            curDest += 3;
+            if (padded) {
+                _GTMDevAssert(destLen >= 4, @"our calc for encoded length was wrong");
+                curDest[0] = kBase64PaddingChar;
+                curDest += 1;
+            }
+            break;
+    }
+    // return the length
+    return (curDest - destBytes);
+}
+
+//
+// baseDecode:srcLen:destBytes:destLen:charset:requirePadding:
+//
+// Decodes the buffer into the larger.  returns the length of the decoded
+// data, or zero for an error.
+// |charset| is the character decoding buffer to use
+//
+// Returns:
+//   the length of the encoded data.  zero if any error.
+//
++(NSUInteger)baseDecode:(const char *)srcBytes
+                 srcLen:(NSUInteger)srcLen
+              destBytes:(char *)destBytes
+                destLen:(NSUInteger)destLen
+                charset:(const char *)charset
+         requirePadding:(BOOL)requirePadding {
+    if (!srcLen || !destLen || !srcBytes || !destBytes) {
+        return 0;
+    }
+    
+    int decode;
+    NSUInteger destIndex = 0;
+    int state = 0;
+    char ch = 0;
+    while (srcLen-- && (ch = *srcBytes++) != 0)  {
+        if (IsSpace(ch))  // Skip whitespace
+            continue;
+        
+        if (ch == kBase64PaddingChar)
+            break;
+        
+        decode = charset[(unsigned int)ch];
+        if (decode == kBase64InvalidChar)
+            return 0;
+        
+        // Four cyphertext characters decode to three bytes.
+        // Therefore we can be in one of four states.
+        switch (state) {
+            case 0:
+                // We're at the beginning of a four-character cyphertext block.
+                // This sets the high six bits of the first byte of the
+                // plaintext block.
+                _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong");
+                destBytes[destIndex] = decode << 2;
+                state = 1;
+                break;
+            case 1:
+                // We're one character into a four-character cyphertext block.
+                // This sets the low two bits of the first plaintext byte,
+                // and the high four bits of the second plaintext byte.
+                _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong");
+                destBytes[destIndex] |= decode >> 4;
+                destBytes[destIndex+1] = (decode & 0x0f) << 4;
+                destIndex++;
+                state = 2;
+                break;
+            case 2:
+                // We're two characters into a four-character cyphertext block.
+                // This sets the low four bits of the second plaintext
+                // byte, and the high two bits of the third plaintext byte.
+                // However, if this is the end of data, and those two
+                // bits are zero, it could be that those two bits are
+                // leftovers from the encoding of data that had a length
+                // of two mod three.
+                _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong");
+                destBytes[destIndex] |= decode >> 2;
+                destBytes[destIndex+1] = (decode & 0x03) << 6;
+                destIndex++;
+                state = 3;
+                break;
+            case 3:
+                // We're at the last character of a four-character cyphertext block.
+                // This sets the low six bits of the third plaintext byte.
+                _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong");
+                destBytes[destIndex] |= decode;
+                destIndex++;
+                state = 0;
+                break;
+        }
+    }
+    
+    // We are done decoding Base-64 chars.  Let's see if we ended
+    //      on a byte boundary, and/or with erroneous trailing characters.
+    if (ch == kBase64PaddingChar) {               // We got a pad char
+        if ((state == 0) || (state == 1)) {
+            return 0;  // Invalid '=' in first or second position
+        }
+        if (srcLen == 0) {
+            if (state == 2) { // We run out of input but we still need another '='
+                return 0;
+            }
+            // Otherwise, we are in state 3 and only need this '='
+        } else {
+            if (state == 2) {  // need another '='
+                while ((ch = *srcBytes++) && (srcLen-- > 0)) {
+                    if (!IsSpace(ch))
+                        break;
+                }
+                if (ch != kBase64PaddingChar) {
+                    return 0;
+                }
+            }
+            // state = 1 or 2, check if all remain padding is space
+            while ((ch = *srcBytes++) && (srcLen-- > 0)) {
+                if (!IsSpace(ch)) {
+                    return 0;
+                }
+            }
+        }
+    } else {
+        // We ended by seeing the end of the string.
+        
+        if (requirePadding) {
+            // If we require padding, then anything but state 0 is an error.
+            if (state != 0) {
+                return 0;
+            }
+        } else {
+            // Make sure we have no partial bytes lying around.  Note that we do not
+            // require trailing '=', so states 2 and 3 are okay too.
+            if (state == 1) {
+                return 0;
+            }
+        }
+    }
+    
+    // If then next piece of output was valid and got written to it means we got a
+    // very carefully crafted input that appeared valid but contains some trailing
+    // bits past the real length, so just toss the thing.
+    if ((destIndex < destLen) &&
+        (destBytes[destIndex] != 0)) {
+        return 0;
+    }
+    
+    return destIndex;
+}
+
+@end

+ 388 - 0
BFCommonKit/Classes/Vendors/DES/GTMDefines.h

@@ -0,0 +1,388 @@
+//
+// GTMDefines.h
+//
+//  Copyright 2008 Google Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License.  You may obtain a copy
+//  of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+//
+
+// ============================================================================
+
+#include <AvailabilityMacros.h>
+#include <TargetConditionals.h>
+
+#if TARGET_OS_IPHONE
+#include <Availability.h>
+#endif //  TARGET_OS_IPHONE
+
+// Not all MAC_OS_X_VERSION_10_X macros defined in past SDKs
+#ifndef MAC_OS_X_VERSION_10_5
+#define MAC_OS_X_VERSION_10_5 1050
+#endif
+#ifndef MAC_OS_X_VERSION_10_6
+#define MAC_OS_X_VERSION_10_6 1060
+#endif
+
+// Not all __IPHONE_X macros defined in past SDKs
+#ifndef __IPHONE_2_1
+#define __IPHONE_2_1 20100
+#endif
+#ifndef __IPHONE_2_2
+#define __IPHONE_2_2 20200
+#endif
+#ifndef __IPHONE_3_0
+#define __IPHONE_3_0 30000
+#endif
+#ifndef __IPHONE_3_1
+#define __IPHONE_3_1 30100
+#endif
+#ifndef __IPHONE_3_2
+#define __IPHONE_3_2 30200
+#endif
+#ifndef __IPHONE_4_0
+#define __IPHONE_4_0 40000
+#endif
+
+// ----------------------------------------------------------------------------
+// CPP symbols that can be overridden in a prefix to control how the toolbox
+// is compiled.
+// ----------------------------------------------------------------------------
+
+
+// By setting the GTM_CONTAINERS_VALIDATION_FAILED_LOG and
+// GTM_CONTAINERS_VALIDATION_FAILED_ASSERT macros you can control what happens
+// when a validation fails. If you implement your own validators, you may want
+// to control their internals using the same macros for consistency.
+#ifndef GTM_CONTAINERS_VALIDATION_FAILED_ASSERT
+#define GTM_CONTAINERS_VALIDATION_FAILED_ASSERT 0
+#endif
+
+// Give ourselves a consistent way to do inlines.  Apple's macros even use
+// a few different actual definitions, so we're based off of the foundation
+// one.
+#if !defined(GTM_INLINE)
+#if defined (__GNUC__) && (__GNUC__ == 4)
+#define GTM_INLINE static __inline__ __attribute__((always_inline))
+#else
+#define GTM_INLINE static __inline__
+#endif
+#endif
+
+// Give ourselves a consistent way of doing externs that links up nicely
+// when mixing objc and objc++
+#if !defined (GTM_EXTERN)
+#if defined __cplusplus
+#define GTM_EXTERN extern "C"
+#define GTM_EXTERN_C_BEGIN extern "C" {
+#define GTM_EXTERN_C_END }
+#else
+#define GTM_EXTERN extern
+#define GTM_EXTERN_C_BEGIN
+#define GTM_EXTERN_C_END
+#endif
+#endif
+
+// Give ourselves a consistent way of exporting things if we have visibility
+// set to hidden.
+#if !defined (GTM_EXPORT)
+#define GTM_EXPORT __attribute__((visibility("default")))
+#endif
+
+// Give ourselves a consistent way of declaring something as unused. This
+// doesn't use __unused because that is only supported in gcc 4.2 and greater.
+#if !defined (GTM_UNUSED)
+#define GTM_UNUSED(x) ((void)(x))
+#endif
+
+// _GTMDevLog & _GTMDevAssert
+//
+// _GTMDevLog & _GTMDevAssert are meant to be a very lightweight shell for
+// developer level errors.  This implementation simply macros to NSLog/NSAssert.
+// It is not intended to be a general logging/reporting system.
+//
+// Please see http://code.google.com/p/google-toolbox-for-mac/wiki/DevLogNAssert
+// for a little more background on the usage of these macros.
+//
+//    _GTMDevLog           log some error/problem in debug builds
+//    _GTMDevAssert        assert if conditon isn't met w/in a method/function
+//                           in all builds.
+//
+// To replace this system, just provide different macro definitions in your
+// prefix header.  Remember, any implementation you provide *must* be thread
+// safe since this could be called by anything in what ever situtation it has
+// been placed in.
+//
+
+// We only define the simple macros if nothing else has defined this.
+#ifndef _GTMDevLog
+
+#ifdef DEBUG
+#define _GTMDevLog(...) NSLog(__VA_ARGS__)
+#else
+#define _GTMDevLog(...) do { } while (0)
+#endif
+
+#endif // _GTMDevLog
+
+#ifndef _GTMDevAssert
+// we directly invoke the NSAssert handler so we can pass on the varargs
+// (NSAssert doesn't have a macro we can use that takes varargs)
+#if !defined(NS_BLOCK_ASSERTIONS)
+#define _GTMDevAssert(condition, ...)                                       \
+do {                                                                      \
+if (!(condition)) {                                                     \
+[[NSAssertionHandler currentHandler]                                  \
+handleFailureInFunction:[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \
+file:[NSString stringWithUTF8String:__FILE__]  \
+lineNumber:__LINE__                                  \
+description:__VA_ARGS__];                             \
+}                                                                       \
+} while(0)
+#else // !defined(NS_BLOCK_ASSERTIONS)
+#define _GTMDevAssert(condition, ...) do { } while (0)
+#endif // !defined(NS_BLOCK_ASSERTIONS)
+
+#endif // _GTMDevAssert
+
+// _GTMCompileAssert
+// _GTMCompileAssert is an assert that is meant to fire at compile time if you
+// want to check things at compile instead of runtime. For example if you
+// want to check that a wchar is 4 bytes instead of 2 you would use
+// _GTMCompileAssert(sizeof(wchar_t) == 4, wchar_t_is_4_bytes_on_OS_X)
+// Note that the second "arg" is not in quotes, and must be a valid processor
+// symbol in it's own right (no spaces, punctuation etc).
+
+// Wrapping this in an #ifndef allows external groups to define their own
+// compile time assert scheme.
+#ifndef _GTMCompileAssert
+// We got this technique from here:
+// http://unixjunkie.blogspot.com/2007/10/better-compile-time-asserts_29.html
+
+#define _GTMCompileAssertSymbolInner(line, msg) _GTMCOMPILEASSERT ## line ## __ ## msg
+#define _GTMCompileAssertSymbol(line, msg) _GTMCompileAssertSymbolInner(line, msg)
+#define _GTMCompileAssert(test, msg) \
+typedef char _GTMCompileAssertSymbol(__LINE__, msg) [ ((test) ? 1 : -1) ]
+#endif // _GTMCompileAssert
+
+// ----------------------------------------------------------------------------
+// CPP symbols defined based on the project settings so the GTM code has
+// simple things to test against w/o scattering the knowledge of project
+// setting through all the code.
+// ----------------------------------------------------------------------------
+
+// Provide a single constant CPP symbol that all of GTM uses for ifdefing
+// iPhone code.
+#if TARGET_OS_IPHONE // iPhone SDK
+// For iPhone specific stuff
+#define GTM_IPHONE_SDK 1
+#if TARGET_IPHONE_SIMULATOR
+#define GTM_IPHONE_SIMULATOR 1
+#else
+#define GTM_IPHONE_DEVICE 1
+#endif  // TARGET_IPHONE_SIMULATOR
+#else
+// For MacOS specific stuff
+#define GTM_MACOS_SDK 1
+#endif
+
+// Some of our own availability macros
+#if GTM_MACOS_SDK
+#define GTM_AVAILABLE_ONLY_ON_IPHONE UNAVAILABLE_ATTRIBUTE
+#define GTM_AVAILABLE_ONLY_ON_MACOS
+#else
+#define GTM_AVAILABLE_ONLY_ON_IPHONE
+#define GTM_AVAILABLE_ONLY_ON_MACOS UNAVAILABLE_ATTRIBUTE
+#endif
+
+// Provide a symbol to include/exclude extra code for GC support.  (This mainly
+// just controls the inclusion of finalize methods).
+#ifndef GTM_SUPPORT_GC
+#if GTM_IPHONE_SDK
+// iPhone never needs GC
+#define GTM_SUPPORT_GC 0
+#else
+// We can't find a symbol to tell if GC is supported/required, so best we
+// do on Mac targets is include it if we're on 10.5 or later.
+#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
+#define GTM_SUPPORT_GC 0
+#else
+#define GTM_SUPPORT_GC 1
+#endif
+#endif
+#endif
+
+// To simplify support for 64bit (and Leopard in general), we provide the type
+// defines for non Leopard SDKs
+#if !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
+// NSInteger/NSUInteger and Max/Mins
+#ifndef NSINTEGER_DEFINED
+#if __LP64__ || NS_BUILD_32_LIKE_64
+typedef long NSInteger;
+typedef unsigned long NSUInteger;
+#else
+typedef int NSInteger;
+typedef unsigned int NSUInteger;
+#endif
+#define NSIntegerMax    LONG_MAX
+#define NSIntegerMin    LONG_MIN
+#define NSUIntegerMax   ULONG_MAX
+#define NSINTEGER_DEFINED 1
+#endif  // NSINTEGER_DEFINED
+// CGFloat
+#ifndef CGFLOAT_DEFINED
+#if defined(__LP64__) && __LP64__
+// This really is an untested path (64bit on Tiger?)
+typedef double CGFloat;
+#define CGFLOAT_MIN DBL_MIN
+#define CGFLOAT_MAX DBL_MAX
+#define CGFLOAT_IS_DOUBLE 1
+#else /* !defined(__LP64__) || !__LP64__ */
+typedef float CGFloat;
+#define CGFLOAT_MIN FLT_MIN
+#define CGFLOAT_MAX FLT_MAX
+#define CGFLOAT_IS_DOUBLE 0
+#endif /* !defined(__LP64__) || !__LP64__ */
+#define CGFLOAT_DEFINED 1
+#endif // CGFLOAT_DEFINED
+#endif  // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
+
+// Some support for advanced clang static analysis functionality
+// See http://clang-analyzer.llvm.org/annotations.html
+#ifndef __has_feature      // Optional.
+#define __has_feature(x) 0 // Compatibility with non-clang compilers.
+#endif
+
+#ifndef NS_RETURNS_RETAINED
+#if __has_feature(attribute_ns_returns_retained)
+#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
+#else
+#define NS_RETURNS_RETAINED
+#endif
+#endif
+
+#ifndef NS_RETURNS_NOT_RETAINED
+#if __has_feature(attribute_ns_returns_not_retained)
+#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
+#else
+#define NS_RETURNS_NOT_RETAINED
+#endif
+#endif
+
+#ifndef CF_RETURNS_RETAINED
+#if __has_feature(attribute_cf_returns_retained)
+#define CF_RETURNS_RETAINED __attribute__((cf_returns_retained))
+#else
+#define CF_RETURNS_RETAINED
+#endif
+#endif
+
+#ifndef CF_RETURNS_NOT_RETAINED
+#if __has_feature(attribute_cf_returns_not_retained)
+#define CF_RETURNS_NOT_RETAINED __attribute__((cf_returns_not_retained))
+#else
+#define CF_RETURNS_NOT_RETAINED
+#endif
+#endif
+
+// Defined on 10.6 and above.
+#ifndef NS_FORMAT_ARGUMENT
+#define NS_FORMAT_ARGUMENT(A)
+#endif
+
+// Defined on 10.6 and above.
+#ifndef NS_FORMAT_FUNCTION
+#define NS_FORMAT_FUNCTION(F,A)
+#endif
+
+// Defined on 10.6 and above.
+#ifndef CF_FORMAT_ARGUMENT
+#define CF_FORMAT_ARGUMENT(A)
+#endif
+
+// Defined on 10.6 and above.
+#ifndef CF_FORMAT_FUNCTION
+#define CF_FORMAT_FUNCTION(F,A)
+#endif
+
+#ifndef GTM_NONNULL
+#define GTM_NONNULL(x) __attribute__((nonnull(x)))
+#endif
+
+#ifdef __OBJC__
+
+// Declared here so that it can easily be used for logging tracking if
+// necessary. See GTMUnitTestDevLog.h for details.
+@class NSString;
+GTM_EXTERN void _GTMUnitTestDevLog(NSString *format, ...);
+
+// Macro to allow you to create NSStrings out of other macros.
+// #define FOO foo
+// NSString *fooString = GTM_NSSTRINGIFY(FOO);
+#if !defined (GTM_NSSTRINGIFY)
+#define GTM_NSSTRINGIFY_INNER(x) @#x
+#define GTM_NSSTRINGIFY(x) GTM_NSSTRINGIFY_INNER(x)
+#endif
+
+// Macro to allow fast enumeration when building for 10.5 or later, and
+// reliance on NSEnumerator for 10.4.  Remember, NSDictionary w/ FastEnumeration
+// does keys, so pick the right thing, nothing is done on the FastEnumeration
+// side to be sure you're getting what you wanted.
+#ifndef GTM_FOREACH_OBJECT
+#if TARGET_OS_IPHONE || !(MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5)
+#define GTM_FOREACH_ENUMEREE(element, enumeration) \
+for (element in enumeration)
+#define GTM_FOREACH_OBJECT(element, collection) \
+for (element in collection)
+#define GTM_FOREACH_KEY(element, collection) \
+for (element in collection)
+#else
+#define GTM_FOREACH_ENUMEREE(element, enumeration) \
+for (NSEnumerator *_ ## element ## _enum = enumeration; \
+(element = [_ ## element ## _enum nextObject]) != nil; )
+#define GTM_FOREACH_OBJECT(element, collection) \
+GTM_FOREACH_ENUMEREE(element, [collection objectEnumerator])
+#define GTM_FOREACH_KEY(element, collection) \
+GTM_FOREACH_ENUMEREE(element, [collection keyEnumerator])
+#endif
+#endif
+
+// ============================================================================
+
+// To simplify support for both Leopard and Snow Leopard we declare
+// the Snow Leopard protocols that we need here.
+#if !defined(GTM_10_6_PROTOCOLS_DEFINED) && !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6)
+#define GTM_10_6_PROTOCOLS_DEFINED 1
+@protocol NSConnectionDelegate
+@end
+@protocol NSAnimationDelegate
+@end
+@protocol NSImageDelegate
+@end
+@protocol NSTabViewDelegate
+@end
+#endif  // !defined(GTM_10_6_PROTOCOLS_DEFINED) && !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6)
+
+// GTM_SEL_STRING is for specifying selector (usually property) names to KVC
+// or KVO methods.
+// In debug it will generate warnings for undeclared selectors if
+// -Wunknown-selector is turned on.
+// In release it will have no runtime overhead.
+#ifndef GTM_SEL_STRING
+#ifdef DEBUG
+#define GTM_SEL_STRING(selName) NSStringFromSelector(@selector(selName))
+#else
+#define GTM_SEL_STRING(selName) @#selName
+#endif  // DEBUG
+#endif  // GTM_SEL_STRING
+
+#endif // __OBJC__

+ 50 - 0
BFCommonKit/Classes/Vendors/FBShimmering/FBShimmering.h

@@ -0,0 +1,50 @@
+/**
+ Copyright (c) 2014-present, Facebook, Inc.
+ All rights reserved.
+ 
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FBShimmerDirection) {
+    FBShimmerDirectionRight,    // Shimmer animation goes from left to right
+    FBShimmerDirectionLeft      // Shimmer animation goes from right to left
+};
+
+@protocol FBShimmering <NSObject>
+
+//! @abstract Set this to YES to start shimming and NO to stop. Defaults to NO.
+@property (nonatomic, assign, readwrite, getter = isShimmering) BOOL shimmering;
+
+//! @abstract The time interval between shimmerings in seconds. Defaults to 0.4.
+@property (assign, nonatomic, readwrite) CFTimeInterval shimmeringPauseDuration;
+
+//! @abstract The opacity of the content while it is shimmering. Defaults to 0.5.
+@property (assign, nonatomic, readwrite) CGFloat shimmeringOpacity;
+
+//! @abstract The speed of shimmering, in points per second. Defaults to 230.
+@property (assign, nonatomic, readwrite) CGFloat shimmeringSpeed;
+
+//! @abstract The highlight width of shimmering. Range of [0,1], defaults to 0.33.
+@property (assign, nonatomic, readwrite) CGFloat shimmeringHighlightWidth;
+
+//! @abstract The direction of shimmering animation. Defaults to FBShimmerDirectionRight.
+@property (assign, nonatomic, readwrite) FBShimmerDirection shimmeringDirection;
+
+//! @abstract The duration of the fade used when shimmer begins. Defaults to 0.1.
+@property (assign, nonatomic, readwrite) CFTimeInterval shimmeringBeginFadeDuration;
+
+//! @abstract The duration of the fade used when shimmer ends. Defaults to 0.3.
+@property (assign, nonatomic, readwrite) CFTimeInterval shimmeringEndFadeDuration;
+
+/**
+ @abstract The absolute CoreAnimation media time when the shimmer will fade in.
+ @discussion Only valid after setting {@ref shimmering} to NO.
+ */
+@property (assign, nonatomic, readonly) CFTimeInterval shimmeringFadeTime;
+
+@end
+

+ 22 - 0
BFCommonKit/Classes/Vendors/FBShimmering/FBShimmeringLayer.h

@@ -0,0 +1,22 @@
+/**
+ Copyright (c) 2014-present, Facebook, Inc.
+ All rights reserved.
+ 
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import <QuartzCore/CALayer.h>
+
+#import "FBShimmering.h"
+
+/**
+  @abstract Lightweight, generic shimmering layer.
+ */
+@interface FBShimmeringLayer : CALayer <FBShimmering>
+
+//! @abstract The content layer to be shimmered.
+@property (strong, nonatomic) CALayer *contentLayer;
+
+@end

+ 433 - 0
BFCommonKit/Classes/Vendors/FBShimmering/FBShimmeringLayer.m

@@ -0,0 +1,433 @@
+/**
+ Copyright (c) 2014-present, Facebook, Inc.
+ All rights reserved.
+ 
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "FBShimmeringLayer.h"
+
+#import <QuartzCore/CAAnimation.h>
+#import <QuartzCore/CAGradientLayer.h>
+#import <QuartzCore/CATransaction.h>
+
+#import <UIKit/UIGeometry.h>
+#import <UIKit/UIColor.h>
+
+#if !__has_feature(objc_arc)
+#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
+#endif
+
+#if TARGET_IPHONE_SIMULATOR
+UIKIT_EXTERN float UIAnimationDragCoefficient(void); // UIKit private drag coeffient, use judiciously
+#endif
+
+static CGFloat FBShimmeringLayerDragCoefficient(void)
+{
+#if TARGET_IPHONE_SIMULATOR
+  return UIAnimationDragCoefficient();
+#else
+  return 1.0;
+#endif
+}
+
+static void FBShimmeringLayerAnimationApplyDragCoefficient(CAAnimation *animation)
+{
+  CGFloat k = FBShimmeringLayerDragCoefficient();
+  
+  if (k != 0 && k != 1) {
+    animation.speed = 1 / k;
+  }
+}
+
+// animations keys
+static NSString *const kFBShimmerSlideAnimationKey = @"slide";
+static NSString *const kFBFadeAnimationKey = @"fade";
+static NSString *const kFBEndFadeAnimationKey = @"fade-end";
+
+static CABasicAnimation *fade_animation(id delegate, CALayer *layer, CGFloat opacity, CFTimeInterval duration)
+{
+  CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+  animation.delegate = delegate;
+  animation.fromValue = @([(layer.presentationLayer ?: layer) opacity]);
+  animation.toValue = @(opacity);
+  animation.fillMode = kCAFillModeBoth;
+  animation.removedOnCompletion = NO;
+  animation.duration = duration;
+  FBShimmeringLayerAnimationApplyDragCoefficient(animation);
+  return animation;
+}
+
+static CABasicAnimation *shimmer_begin_fade_animation(id delegate, CALayer *layer, CGFloat opacity, CGFloat duration)
+{
+  return fade_animation(delegate, layer, opacity, duration);
+}
+
+static CABasicAnimation *shimmer_end_fade_animation(id delegate, CALayer *layer, CGFloat opacity, CGFloat duration)
+{
+  CABasicAnimation *animation = fade_animation(delegate, layer, opacity, duration);
+  [animation setValue:@YES forKey:kFBEndFadeAnimationKey];
+  return animation;
+}
+
+static CABasicAnimation *shimmer_slide_animation(id delegate, CFTimeInterval duration, FBShimmerDirection direction)
+{
+  CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
+  animation.delegate = delegate;
+  animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
+  animation.duration = duration;
+  animation.repeatCount = HUGE_VALF;
+  FBShimmeringLayerAnimationApplyDragCoefficient(animation);
+  if (direction == FBShimmerDirectionLeft) {
+    animation.speed = -abs(animation.speed);
+  }
+  return animation;
+}
+
+// take a shimmer slide animation and turns into repeating
+static CAAnimation *shimmer_slide_repeat(CAAnimation *a, CFTimeInterval duration, FBShimmerDirection direction)
+{
+  CAAnimation *anim = [a copy];
+  anim.repeatCount = HUGE_VALF;
+  anim.duration = duration;
+  anim.speed = (direction == FBShimmerDirectionRight) ? abs(anim.speed) : -abs(anim.speed);
+  return anim;
+}
+
+// take a shimmer slide animation and turns into finish
+static CAAnimation *shimmer_slide_finish(CAAnimation *a)
+{
+  CAAnimation *anim = [a copy];
+  anim.repeatCount = 0;
+  return anim;
+}
+
+@interface FBShimmeringMaskLayer : CAGradientLayer
+@property (readonly, nonatomic) CALayer *fadeLayer;
+@end
+
+@implementation FBShimmeringMaskLayer
+
+- (instancetype)init
+{
+  self = [super init];
+  if (nil != self) {
+    _fadeLayer = [[CALayer alloc] init];
+    _fadeLayer.backgroundColor = [UIColor whiteColor].CGColor;
+    [self addSublayer:_fadeLayer];
+  }
+  return self;
+}
+
+- (void)layoutSublayers
+{
+  [super layoutSublayers];
+  CGRect r = self.bounds;
+  _fadeLayer.bounds = r;
+  _fadeLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
+}
+
+@end
+
+@interface FBShimmeringLayer ()
+@property (strong, nonatomic) FBShimmeringMaskLayer *maskLayer;
+@end
+
+@implementation FBShimmeringLayer
+{
+  CALayer *_contentLayer;
+  FBShimmeringMaskLayer *_maskLayer;
+}
+
+#pragma mark - Lifecycle
+
+@synthesize shimmering = _shimmering;
+@synthesize shimmeringPauseDuration = _shimmeringPauseDuration;
+@synthesize shimmeringOpacity = _shimmeringOpacity;
+@synthesize shimmeringSpeed = _shimmeringSpeed;
+@synthesize shimmeringHighlightWidth = _shimmeringHighlightWidth;
+@synthesize shimmeringDirection = _shimmeringDirection;
+@synthesize shimmeringFadeTime = _shimmeringFadeTime;
+@synthesize shimmeringBeginFadeDuration = _shimmeringBeginFadeDuration;
+@synthesize shimmeringEndFadeDuration = _shimmeringEndFadeDuration;
+
+- (instancetype)init
+{
+  self = [super init];
+  if (nil != self) {
+    // default configuration
+    _shimmeringPauseDuration = 0.4;
+    _shimmeringSpeed = 230.0;
+    _shimmeringHighlightWidth = 1.0;
+    _shimmeringOpacity = 0.5;
+    _shimmeringDirection = FBShimmerDirectionRight;
+    _shimmeringBeginFadeDuration = 0.1;
+    _shimmeringEndFadeDuration = 0.3;
+  }
+  return self;
+}
+
+#pragma mark - Properties
+
+- (void)setContentLayer:(CALayer *)contentLayer
+{
+  // reset mask
+  self.maskLayer = nil;
+
+  // note content layer and add for display
+  _contentLayer = contentLayer;
+  self.sublayers = contentLayer ? @[contentLayer] : nil;
+
+  // update shimmering animation
+  [self _updateShimmering];
+}
+
+- (void)setShimmering:(BOOL)shimmering
+{
+  if (shimmering != _shimmering) {
+    _shimmering = shimmering;
+    [self _updateShimmering];
+  }
+}
+
+- (void)setShimmeringSpeed:(CGFloat)speed
+{
+  if (speed != _shimmeringSpeed) {
+    _shimmeringSpeed = speed;
+    [self _updateShimmering];
+  }
+}
+
+- (void)setShimmeringHighlightWidth:(CGFloat)width
+{
+  if (width != _shimmeringHighlightWidth) {
+    _shimmeringHighlightWidth = width;
+    [self _updateShimmering];
+  }
+}
+
+- (void)setShimmeringDirection:(FBShimmerDirection)direction
+{
+  if (direction != _shimmeringDirection) {
+    _shimmeringDirection = direction;
+    [self _updateShimmering];
+  }
+}
+
+- (void)setShimmeringPauseDuration:(CFTimeInterval)duration
+{
+  if (duration != _shimmeringPauseDuration) {
+    _shimmeringPauseDuration = duration;
+    [self _updateShimmering];
+  }
+}
+
+- (void)setShimmeringOpacity:(CGFloat)shimmeringOpacity
+{
+  if (shimmeringOpacity != _shimmeringOpacity) {
+    _shimmeringOpacity = shimmeringOpacity;
+    [self _updateMaskColors];
+  }
+}
+
+- (void)layoutSublayers
+{
+  CGRect r = self.bounds;
+  _contentLayer.anchorPoint = CGPointMake(0.5, 0.5);
+  _contentLayer.bounds = r;
+  _contentLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
+  
+  if (nil != _maskLayer) {
+    [self _updateMaskLayout];
+  }
+}
+
+- (void)setBounds:(CGRect)bounds
+{
+  if (!CGRectEqualToRect(self.bounds, bounds)) {
+    [super setBounds:bounds];
+
+    [self _updateShimmering];
+  }
+}
+
+#pragma mark - Internal
+
+- (void)_clearMask
+{
+  if (nil == _maskLayer) {
+    return;
+  }
+
+  BOOL disableActions = [CATransaction disableActions];
+  [CATransaction setDisableActions:YES];
+
+  self.maskLayer = nil;
+  _contentLayer.mask = nil;
+  
+  [CATransaction setDisableActions:disableActions];
+}
+
+- (void)_createMaskIfNeeded
+{
+  if (_shimmering && !_maskLayer) {
+    _maskLayer = [FBShimmeringMaskLayer layer];
+    _maskLayer.delegate = self;
+    _contentLayer.mask = _maskLayer;
+    [self _updateMaskColors];
+    [self _updateMaskLayout];
+  }
+}
+
+- (void)_updateMaskColors
+{
+  if (nil == _maskLayer) {
+    return;
+  }
+
+  // We create a gradient to be used as a mask.
+  // In a mask, the colors do not matter, it's the alpha that decides the degree of masking.
+  UIColor *maskedColor = [UIColor colorWithWhite:1.0 alpha:_shimmeringOpacity];
+  UIColor *unmaskedColor = [UIColor whiteColor];
+
+  // Create a gradient from masked to unmasked to masked.
+  _maskLayer.colors = @[(__bridge id)maskedColor.CGColor, (__bridge id)unmaskedColor.CGColor, (__bridge id)maskedColor.CGColor];
+}
+
+- (void)_updateMaskLayout
+{
+  // Everything outside the mask layer is hidden, so we need to create a mask long enough for the shimmered layer to be always covered by the mask.
+  CGFloat width = CGRectGetWidth(_contentLayer.bounds);
+  if (0 == width) {
+    return;
+  }
+
+  // extra distance for the gradient to travel during the pause.
+  CGFloat extraDistance = width + _shimmeringSpeed * _shimmeringPauseDuration;
+
+  // compute how far the shimmering goes
+  CGFloat fullShimmerLength = width * 3.0f + extraDistance;
+  CGFloat travelDistance = width * 2.0f + extraDistance;
+
+  // setup the gradient for shimmering
+  _maskLayer.startPoint = CGPointMake((width + extraDistance) / fullShimmerLength, 0.0);
+  _maskLayer.endPoint = CGPointMake(travelDistance / fullShimmerLength, 0.0);
+  
+  // position the gradient for the desired width
+  CGFloat highlightOutsideWidth = (1.0 - _shimmeringHighlightWidth) / 2.0;
+  _maskLayer.locations = @[@(highlightOutsideWidth),
+                           @(0.5),
+                           @(1.0 - highlightOutsideWidth)];
+  
+  // position for the start of the animation
+  _maskLayer.anchorPoint = CGPointZero;
+  _maskLayer.position = CGPointMake(-travelDistance, 0.0);
+  _maskLayer.bounds = CGRectMake(0.0, 0.0, fullShimmerLength, CGRectGetHeight(_contentLayer.bounds));
+}
+
+- (void)_updateShimmering
+{
+  // create mask if needed
+  [self _createMaskIfNeeded];
+
+  // if not shimmering and no mask, noop
+  if (!_shimmering && !_maskLayer) {
+    return;
+  }
+
+  // ensure layed out
+  [self layoutIfNeeded];
+
+  BOOL disableActions = [CATransaction disableActions];
+  if (!_shimmering) {
+    if (disableActions) {
+      // simply remove mask
+      [self _clearMask];
+    } else {
+      // end slide
+      CFTimeInterval slideEndTime = 0;
+
+      CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
+      if (slideAnimation != nil) {
+        // determing total time sliding
+        CFTimeInterval now = CACurrentMediaTime();
+        CFTimeInterval slideTotalDuration = now - slideAnimation.beginTime;
+
+        // determine time offset into current slide
+        CFTimeInterval slideTimeOffset = fmod(slideTotalDuration, slideAnimation.duration);
+
+        // transition to non-repeating slide
+        CAAnimation *finishAnimation = shimmer_slide_finish(slideAnimation);
+
+        // adjust begin time to now - offset
+        finishAnimation.beginTime = now - slideTimeOffset;
+
+        // note slide end time and begin
+        slideEndTime = finishAnimation.beginTime + slideAnimation.duration;
+        [_maskLayer addAnimation:finishAnimation forKey:kFBShimmerSlideAnimationKey];
+      }
+
+      // fade in text at slideEndTime
+      CABasicAnimation *fadeInAnimation = shimmer_end_fade_animation(self, _maskLayer.fadeLayer, 1.0, _shimmeringEndFadeDuration);
+      fadeInAnimation.beginTime = slideEndTime;
+      [_maskLayer.fadeLayer addAnimation:fadeInAnimation forKey:kFBFadeAnimationKey];
+
+      // expose end time for synchronization
+      _shimmeringFadeTime = slideEndTime;
+    }
+  } else {
+    // fade out text, optionally animated
+    CABasicAnimation *fadeOutAnimation = nil;
+    if (_shimmeringBeginFadeDuration > 0.0 && !disableActions) {
+      fadeOutAnimation = shimmer_begin_fade_animation(self, _maskLayer.fadeLayer, 0.0, _shimmeringBeginFadeDuration);
+      [_maskLayer.fadeLayer addAnimation:fadeOutAnimation forKey:kFBFadeAnimationKey];
+    } else {
+      BOOL disableActions = [CATransaction disableActions];
+      [CATransaction setDisableActions:YES];
+
+      _maskLayer.fadeLayer.opacity = 0.0;
+      [_maskLayer.fadeLayer removeAllAnimations];
+      
+      [CATransaction setDisableActions:disableActions];
+    }
+
+    // begin slide animation
+    CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
+    
+    // compute shimmer duration
+    CFTimeInterval animationDuration = (CGRectGetWidth(_contentLayer.bounds) / _shimmeringSpeed) + _shimmeringPauseDuration;
+    
+    if (slideAnimation != nil) {
+      // ensure existing slide animation repeats
+      [_maskLayer addAnimation:shimmer_slide_repeat(slideAnimation, animationDuration, _shimmeringDirection) forKey:kFBShimmerSlideAnimationKey];
+    } else {
+      // add slide animation
+      slideAnimation = shimmer_slide_animation(self, animationDuration, _shimmeringDirection);
+      slideAnimation.fillMode = kCAFillModeForwards;
+      slideAnimation.removedOnCompletion = NO;
+      slideAnimation.beginTime = CACurrentMediaTime() + fadeOutAnimation.duration;
+      [_maskLayer addAnimation:slideAnimation forKey:kFBShimmerSlideAnimationKey];
+    }
+  }
+}
+
+#pragma mark - CALayerDelegate
+
+- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
+{
+  // no associated actions
+  return (id)kCFNull;
+}
+
+#pragma mark - CAAnimationDelegate
+
+- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
+{
+  if (flag && [[anim valueForKey:kFBEndFadeAnimationKey] boolValue]) {
+    [self _clearMask];
+  }
+}
+
+@end

+ 22 - 0
BFCommonKit/Classes/Vendors/FBShimmering/FBShimmeringView.h

@@ -0,0 +1,22 @@
+/**
+ Copyright (c) 2014-present, Facebook, Inc.
+ All rights reserved.
+ 
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import <UIKit/UIView.h>
+
+#import "FBShimmering.h"
+
+/**
+  @abstract Lightweight, generic shimmering view.
+ */
+@interface FBShimmeringView : UIView <FBShimmering>
+
+//! @abstract The content view to be shimmered.
+@property (strong, nonatomic) UIView *contentView;
+
+@end

+ 60 - 0
BFCommonKit/Classes/Vendors/FBShimmering/FBShimmeringView.m

@@ -0,0 +1,60 @@
+/**
+ Copyright (c) 2014-present, Facebook, Inc.
+ All rights reserved.
+ 
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "FBShimmeringView.h"
+
+#import "FBShimmeringLayer.h"
+
+#if !__has_feature(objc_arc)
+#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
+#endif
+
+@implementation FBShimmeringView
+
++ (Class)layerClass
+{
+  return [FBShimmeringLayer class];
+}
+
+#define __layer ((FBShimmeringLayer *)self.layer)
+
+#define LAYER_ACCESSOR(accessor, ctype) \
+- (ctype)accessor { \
+  return [__layer accessor]; \
+}
+
+#define LAYER_MUTATOR(mutator, ctype) \
+- (void)mutator (ctype)value { \
+  [__layer mutator value]; \
+}
+
+#define LAYER_RW_PROPERTY(accessor, mutator, ctype) \
+  LAYER_ACCESSOR (accessor, ctype) \
+  LAYER_MUTATOR (mutator, ctype)
+
+LAYER_RW_PROPERTY(isShimmering, setShimmering:, BOOL)
+LAYER_RW_PROPERTY(shimmeringPauseDuration, setShimmeringPauseDuration:, CFTimeInterval)
+LAYER_RW_PROPERTY(shimmeringOpacity, setShimmeringOpacity:, CGFloat)
+LAYER_RW_PROPERTY(shimmeringSpeed, setShimmeringSpeed:, CGFloat)
+LAYER_RW_PROPERTY(shimmeringHighlightWidth, setShimmeringHighlightWidth:, CGFloat)
+LAYER_RW_PROPERTY(shimmeringDirection, setShimmeringDirection:, FBShimmerDirection)
+LAYER_ACCESSOR(shimmeringFadeTime, CFTimeInterval)
+LAYER_RW_PROPERTY(shimmeringBeginFadeDuration, setShimmeringBeginFadeDuration:, CFTimeInterval)
+LAYER_RW_PROPERTY(shimmeringEndFadeDuration, setShimmeringEndFadeDuration:, CFTimeInterval)
+
+- (void)setContentView:(UIView *)contentView
+{
+  if (contentView != _contentView) {
+    _contentView = contentView;
+    [self addSubview:contentView];
+    __layer.contentLayer = contentView.layer;
+  }
+}
+
+@end

+ 2 - 2
Example/BFCommonKit.xcodeproj/project.pbxproj

@@ -432,7 +432,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -478,7 +478,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";