You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

321 lines
11 KiB

//
// ImageProgressive.swift
// Kingfisher
//
// Created by lixiang on 2019/5/10.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
import CoreGraphics
private let sharedProcessingQueue: CallbackQueue =
.dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process"))
public struct ImageProgressive {
/// A default `ImageProgressive` could be used across.
public static let `default` = ImageProgressive(
isBlur: true,
isFastestScan: true,
scanInterval: 0
)
/// Whether to enable blur effect processing
let isBlur: Bool
/// Whether to enable the fastest scan
let isFastestScan: Bool
/// Minimum time interval for each scan
let scanInterval: TimeInterval
public init(isBlur: Bool,
isFastestScan: Bool,
scanInterval: TimeInterval) {
self.isBlur = isBlur
self.isFastestScan = isFastestScan
self.scanInterval = scanInterval
}
}
protocol ImageSettable: AnyObject {
var image: KFCrossPlatformImage? { get set }
}
final class ImageProgressiveProvider: DataReceivingSideEffect {
var onShouldApply: () -> Bool = { return true }
func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
DispatchQueue.main.async {
guard self.onShouldApply() else { return }
self.update(data: task.mutableData, with: task.callbacks)
}
}
private let option: ImageProgressive
private let refresh: (KFCrossPlatformImage) -> Void
private let decoder: ImageProgressiveDecoder
private let queue = ImageProgressiveSerialQueue()
init?(_ options: KingfisherParsedOptionsInfo,
refresh: @escaping (KFCrossPlatformImage) -> Void) {
guard let option = options.progressiveJPEG else { return nil }
self.option = option
self.refresh = refresh
self.decoder = ImageProgressiveDecoder(
option,
processingQueue: options.processingQueue ?? sharedProcessingQueue,
creatingOptions: options.imageCreatingOptions
)
}
func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) {
guard !data.isEmpty else { return }
queue.add(minimum: option.scanInterval) { completion in
func decode(_ data: Data) {
self.decoder.decode(data, with: callbacks) { image in
defer { completion() }
guard self.onShouldApply() else { return }
guard let image = image else { return }
self.refresh(image)
}
}
let semaphore = DispatchSemaphore(value: 0)
var onShouldApply: Bool = false
CallbackQueue.mainAsync.execute {
onShouldApply = self.onShouldApply()
semaphore.signal()
}
semaphore.wait()
guard onShouldApply else {
self.queue.clean()
completion()
return
}
if self.option.isFastestScan {
decode(self.decoder.scanning(data) ?? Data())
} else {
self.decoder.scanning(data).forEach { decode($0) }
}
}
}
}
private final class ImageProgressiveDecoder {
private let option: ImageProgressive
private let processingQueue: CallbackQueue
private let creatingOptions: ImageCreatingOptions
private(set) var scannedCount = 0
private(set) var scannedIndex = -1
init(_ option: ImageProgressive,
processingQueue: CallbackQueue,
creatingOptions: ImageCreatingOptions) {
self.option = option
self.processingQueue = processingQueue
self.creatingOptions = creatingOptions
}
func scanning(_ data: Data) -> [Data] {
guard data.kf.contains(jpeg: .SOF2) else {
return []
}
guard scannedIndex + 1 < data.count else {
return []
}
var datas: [Data] = []
var index = scannedIndex + 1
var count = scannedCount
while index < data.count - 1 {
scannedIndex = index
// 0xFF, 0xDA - Start Of Scan
let SOS = ImageFormat.JPEGMarker.SOS.bytes
if data[index] == SOS[0], data[index + 1] == SOS[1] {
if count > 0 {
datas.append(data[0 ..< index])
}
count += 1
}
index += 1
}
// Found more scans this the previous time
guard count > scannedCount else { return [] }
scannedCount = count
// `> 1` checks that we've received a first scan (SOS) and then received
// and also received a second scan (SOS). This way we know that we have
// at least one full scan available.
guard count > 1 else { return [] }
return datas
}
func scanning(_ data: Data) -> Data? {
guard data.kf.contains(jpeg: .SOF2) else {
return nil
}
guard scannedIndex + 1 < data.count else {
return nil
}
var index = scannedIndex + 1
var count = scannedCount
var lastSOSIndex = 0
while index < data.count - 1 {
scannedIndex = index
// 0xFF, 0xDA - Start Of Scan
let SOS = ImageFormat.JPEGMarker.SOS.bytes
if data[index] == SOS[0], data[index + 1] == SOS[1] {
lastSOSIndex = index
count += 1
}
index += 1
}
// Found more scans this the previous time
guard count > scannedCount else { return nil }
scannedCount = count
// `> 1` checks that we've received a first scan (SOS) and then received
// and also received a second scan (SOS). This way we know that we have
// at least one full scan available.
guard count > 1 && lastSOSIndex > 0 else { return nil }
return data[0 ..< lastSOSIndex]
}
func decode(_ data: Data,
with callbacks: [SessionDataTask.TaskCallback],
completion: @escaping (KFCrossPlatformImage?) -> Void) {
guard data.kf.contains(jpeg: .SOF2) else {
CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }
return
}
func processing(_ data: Data) {
let processor = ImageDataProcessor(
data: data,
callbacks: callbacks,
processingQueue: processingQueue
)
processor.onImageProcessed.delegate(on: self) { (self, result) in
guard let image = try? result.0.get() else {
CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }
return
}
CallbackQueue.mainCurrentOrAsync.execute { completion(image) }
}
processor.process()
}
// Blur partial images.
let count = scannedCount
if option.isBlur, count < 6 {
processingQueue.execute {
// Progressively reduce blur as we load more scans.
let image = KingfisherWrapper<KFCrossPlatformImage>.image(
data: data,
options: self.creatingOptions
)
let radius = max(2, 14 - count * 4)
let temp = image?.kf.blurred(withRadius: CGFloat(radius))
processing(temp?.kf.data(format: .JPEG) ?? data)
}
} else {
processing(data)
}
}
}
private final class ImageProgressiveSerialQueue {
typealias ClosureCallback = ((@escaping () -> Void)) -> Void
private let queue: DispatchQueue = .init(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue")
private var items: [DispatchWorkItem] = []
private var notify: (() -> Void)?
private var lastTime: TimeInterval?
var count: Int { return items.count }
func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) {
let completion = { [weak self] in
guard let self = self else { return }
self.queue.async { [weak self] in
guard let self = self else { return }
guard !self.items.isEmpty else { return }
self.items.removeFirst()
if let next = self.items.first {
self.queue.asyncAfter(
deadline: .now() + interval,
execute: next
)
} else {
self.lastTime = Date().timeIntervalSince1970
self.notify?()
self.notify = nil
}
}
}
queue.async { [weak self] in
guard let self = self else { return }
let item = DispatchWorkItem {
closure(completion)
}
if self.items.isEmpty {
let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0)
let delay = difference < interval ? interval - difference : 0
self.queue.asyncAfter(deadline: .now() + delay, execute: item)
}
self.items.append(item)
}
}
func notify(_ closure: @escaping () -> Void) {
self.notify = closure
}
func clean() {
queue.async { [weak self] in
guard let self = self else { return }
self.items.forEach { $0.cancel() }
self.items.removeAll()
}
}
}