|
//
// Request+AlamofireImage.swift
//
// Copyright (c) 2015-2017 Alamofire Software Foundation (http://alamofire.org/)
//
// 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 Alamofire
import Foundation
#if os(iOS) || os(tvOS)
import UIKit
#elseif os(watchOS)
import UIKit
import WatchKit
#elseif os(macOS)
import Cocoa
#endif
extension DataRequest {
static var acceptableImageContentTypes: Set<String> = [
"image/tiff",
"image/jpeg",
"image/gif",
"image/png",
"image/ico",
"image/x-icon",
"image/bmp",
"image/x-bmp",
"image/x-xbitmap",
"image/x-ms-bmp",
"image/x-win-bitmap"
]
static let streamImageInitialBytePattern = Data(bytes: [255, 216]) // 0xffd8
/// Adds the content types specified to the list of acceptable images content types for validation.
///
/// - parameter contentTypes: The additional content types.
public class func addAcceptableImageContentTypes(_ contentTypes: Set<String>) {
DataRequest.acceptableImageContentTypes.formUnion(contentTypes)
}
// MARK: - iOS, tvOS and watchOS
#if os(iOS) || os(tvOS) || os(watchOS)
/// Creates a response serializer that returns an image initialized from the response data using the specified
/// image options.
///
/// - parameter imageScale: The scale factor used when interpreting the image data to construct
/// `responseImage`. Specifying a scale factor of 1.0 results in an image whose
/// size matches the pixel-based dimensions of the image. Applying a different
/// scale factor changes the size of the image as reported by the size property.
/// `Screen.scale` by default.
/// - parameter inflateResponseImage: Whether to automatically inflate response image data for compressed formats
/// (such as PNG or JPEG). Enabling this can significantly improve drawing
/// performance as it allows a bitmap representation to be constructed in the
/// background rather than on the main thread. `true` by default.
///
/// - returns: An image response serializer.
public class func imageResponseSerializer(
imageScale: CGFloat = DataRequest.imageScale,
inflateResponseImage: Bool = true)
-> DataResponseSerializer<Image>
{
return DataResponseSerializer { request, response, data, error in
let result = serializeResponseData(response: response, data: data, error: error)
guard case let .success(data) = result else { return .failure(result.error!) }
do {
try DataRequest.validateContentType(for: request, response: response)
let image = try DataRequest.image(from: data, withImageScale: imageScale)
if inflateResponseImage { image.af_inflate() }
return .success(image)
} catch {
return .failure(error)
}
}
}
/// Adds a response handler to be called once the request has finished.
///
/// - parameter imageScale: The scale factor used when interpreting the image data to construct
/// `responseImage`. Specifying a scale factor of 1.0 results in an image whose
/// size matches the pixel-based dimensions of the image. Applying a different
/// scale factor changes the size of the image as reported by the size property.
/// This is set to the value of scale of the main screen by default, which
/// automatically scales images for retina displays, for instance.
/// `Screen.scale` by default.
/// - parameter inflateResponseImage: Whether to automatically inflate response image data for compressed formats
/// (such as PNG or JPEG). Enabling this can significantly improve drawing
/// performance as it allows a bitmap representation to be constructed in the
/// background rather than on the main thread. `true` by default.
/// - parameter queue: The queue on which the completion handler is dispatched. `nil` by default,
/// which results in using `DispatchQueue.main`.
/// - parameter completionHandler: A closure to be executed once the request has finished. The closure takes 4
/// arguments: the URL request, the URL response, if one was received, the image,
/// if one could be created from the URL response and data, and any error produced
/// while creating the image.
///
/// - returns: The request.
@discardableResult
public func responseImage(
imageScale: CGFloat = DataRequest.imageScale,
inflateResponseImage: Bool = true,
queue: DispatchQueue? = nil,
completionHandler: @escaping (DataResponse<Image>) -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.imageResponseSerializer(
imageScale: imageScale,
inflateResponseImage: inflateResponseImage
),
completionHandler: completionHandler
)
}
/// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server
/// and converted into images.
///
/// - parameter imageScale: The scale factor used when interpreting the image data to construct
/// `responseImage`. Specifying a scale factor of 1.0 results in an image whose
/// size matches the pixel-based dimensions of the image. Applying a different
/// scale factor changes the size of the image as reported by the size property.
/// This is set to the value of scale of the main screen by default, which
/// automatically scales images for retina displays, for instance.
/// `Screen.scale` by default.
/// - parameter inflateResponseImage: Whether to automatically inflate response image data for compressed formats
/// (such as PNG or JPEG). Enabling this can significantly improve drawing
/// performance as it allows a bitmap representation to be constructed in the
/// background rather than on the main thread. `true` by default.
/// - parameter completionHandler: A closure to be executed when the request has new image. The closure takes 1
/// argument: the image, if one could be created from the data.
///
/// - returns: The request.
@discardableResult
public func streamImage(
imageScale: CGFloat = DataRequest.imageScale,
inflateResponseImage: Bool = true,
completionHandler: @escaping (Image) -> Void)
-> Self
{
var imageData = Data()
return stream { chunkData in
if chunkData.starts(with: DataRequest.streamImageInitialBytePattern) {
imageData = Data()
}
imageData.append(chunkData)
if let image = DataRequest.serializeImage(from: imageData) {
completionHandler(image)
}
}
}
private class func serializeImage(
from data: Data,
imageScale: CGFloat = DataRequest.imageScale,
inflateResponseImage: Bool = true)
-> UIImage?
{
guard data.count > 0 else { return nil }
do {
let image = try DataRequest.image(from: data, withImageScale: imageScale)
if inflateResponseImage { image.af_inflate() }
return image
} catch {
return nil
}
}
private class func image(from data: Data, withImageScale imageScale: CGFloat) throws -> UIImage {
if let image = UIImage.af_threadSafeImage(with: data, scale: imageScale) {
return image
}
throw AFIError.imageSerializationFailed
}
public class var imageScale: CGFloat {
#if os(iOS) || os(tvOS)
return UIScreen.main.scale
#elseif os(watchOS)
return WKInterfaceDevice.current().screenScale
#endif
}
#elseif os(macOS)
// MARK: - macOS
/// Creates a response serializer that returns an image initialized from the response data.
///
/// - returns: An image response serializer.
public class func imageResponseSerializer() -> DataResponseSerializer<Image> {
return DataResponseSerializer { request, response, data, error in
let result = serializeResponseData(response: response, data: data, error: error)
guard case let .success(data) = result else { return .failure(result.error!) }
do {
try DataRequest.validateContentType(for: request, response: response)
} catch {
return .failure(error)
}
guard let bitmapImage = NSBitmapImageRep(data: data) else {
return .failure(AFIError.imageSerializationFailed)
}
let image = NSImage(size: NSSize(width: bitmapImage.pixelsWide, height: bitmapImage.pixelsHigh))
image.addRepresentation(bitmapImage)
return .success(image)
}
}
/// Adds a response handler to be called once the request has finished.
///
/// - parameter completionHandler: A closure to be executed once the request has finished. The closure takes 4
/// arguments: the URL request, the URL response, if one was received, the image, if
/// one could be created from the URL response and data, and any error produced while
/// creating the image.
/// - parameter queue: The queue on which the completion handler is dispatched. `nil` by default,
/// which results in using `DispatchQueue.main`.
///
/// - returns: The request.
@discardableResult
public func responseImage(
queue: DispatchQueue? = nil,
completionHandler: @escaping (DataResponse<Image>) -> Void)
-> Self {
return response(
queue: queue,
responseSerializer: DataRequest.imageResponseSerializer(),
completionHandler: completionHandler
)
}
/// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server
/// and converted into images.
///
/// - parameter completionHandler: A closure to be executed when the request has new image. The closure takes 1
/// argument: the image, if one could be created from the data.
///
/// - returns: The request.
@discardableResult
public func streamImage(completionHandler: @escaping (Image) -> Void) -> Self {
var imageData = Data()
return stream { chunkData in
if chunkData.starts(with: DataRequest.streamImageInitialBytePattern) {
imageData = Data()
}
imageData.append(chunkData)
if let image = DataRequest.serializeImage(from: imageData) {
completionHandler(image)
}
}
}
private class func serializeImage(from data: Data) -> NSImage? {
guard data.count > 0 else { return nil }
guard let bitmapImage = NSBitmapImageRep(data: data) else { return nil }
let image = NSImage(size: NSSize(width: bitmapImage.pixelsWide, height: bitmapImage.pixelsHigh))
image.addRepresentation(bitmapImage)
return image
}
#endif
// MARK: - Content Type Validation
/// Returns whether the content type of the response matches one of the acceptable content types.
///
/// - parameter request: The request.
/// - parameter response: The server response.
///
/// - throws: An `AFError` response validation failure when an error is encountered.
public class func validateContentType(for request: URLRequest?, response: HTTPURLResponse?) throws {
if let url = request?.url, url.isFileURL { return }
guard let mimeType = response?.mimeType else {
let contentTypes = Array(DataRequest.acceptableImageContentTypes)
throw AFError.responseValidationFailed(reason: .missingContentType(acceptableContentTypes: contentTypes))
}
guard DataRequest.acceptableImageContentTypes.contains(mimeType) else {
let contentTypes = Array(DataRequest.acceptableImageContentTypes)
throw AFError.responseValidationFailed(
reason: .unacceptableContentType(acceptableContentTypes: contentTypes, responseContentType: mimeType)
)
}
}
}
|