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.
 
 
 
 

377 lines
15 KiB

//
// Image.swift
// Kingfisher
//
// Created by Wei Wang on 16/1/6.
//
// 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.
#if os(macOS)
import AppKit
private var imagesKey: Void?
private var durationKey: Void?
#else
import UIKit
import MobileCoreServices
private var imageSourceKey: Void?
#endif
#if !os(watchOS)
import CoreImage
#endif
import CoreGraphics
import ImageIO
private var animatedImageDataKey: Void?
// MARK: - Image Properties
extension KingfisherWrapper where Base: KFCrossPlatformImage {
private(set) var animatedImageData: Data? {
get { return getAssociatedObject(base, &animatedImageDataKey) }
set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) }
}
#if os(macOS)
var cgImage: CGImage? {
return base.cgImage(forProposedRect: nil, context: nil, hints: nil)
}
var scale: CGFloat {
return 1.0
}
private(set) var images: [KFCrossPlatformImage]? {
get { return getAssociatedObject(base, &imagesKey) }
set { setRetainedAssociatedObject(base, &imagesKey, newValue) }
}
private(set) var duration: TimeInterval {
get { return getAssociatedObject(base, &durationKey) ?? 0.0 }
set { setRetainedAssociatedObject(base, &durationKey, newValue) }
}
var size: CGSize {
return base.representations.reduce(.zero) { size, rep in
let width = max(size.width, CGFloat(rep.pixelsWide))
let height = max(size.height, CGFloat(rep.pixelsHigh))
return CGSize(width: width, height: height)
}
}
#else
var cgImage: CGImage? { return base.cgImage }
var scale: CGFloat { return base.scale }
var images: [KFCrossPlatformImage]? { return base.images }
var duration: TimeInterval { return base.duration }
var size: CGSize { return base.size }
private(set) var imageSource: CGImageSource? {
get { return getAssociatedObject(base, &imageSourceKey) }
set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) }
}
#endif
// Bitmap memory cost with bytes.
var cost: Int {
let pixel = Int(size.width * size.height * scale * scale)
guard let cgImage = cgImage else {
return pixel * 4
}
return pixel * cgImage.bitsPerPixel / 8
}
}
// MARK: - Image Conversion
extension KingfisherWrapper where Base: KFCrossPlatformImage {
#if os(macOS)
static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {
return KFCrossPlatformImage(cgImage: cgImage, size: .zero)
}
/// Normalize the image. This getter does nothing on macOS but return the image itself.
public var normalized: KFCrossPlatformImage { return base }
#else
/// Creating an image from a give `CGImage` at scale and orientation for refImage. The method signature is for
/// compatibility of macOS version.
static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {
return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up)
}
/// Returns normalized image for current `base` image.
/// This method will try to redraw an image with orientation and scale considered.
public var normalized: KFCrossPlatformImage {
// prevent animated image (GIF) lose it's images
guard images == nil else { return base.copy() as! KFCrossPlatformImage }
// No need to do anything if already up
guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage }
return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) {
fixOrientation(in: $0)
return true
}
}
func fixOrientation(in context: CGContext) {
var transform = CGAffineTransform.identity
let orientation = base.imageOrientation
switch orientation {
case .down, .downMirrored:
transform = transform.translatedBy(x: size.width, y: size.height)
transform = transform.rotated(by: .pi)
case .left, .leftMirrored:
transform = transform.translatedBy(x: size.width, y: 0)
transform = transform.rotated(by: .pi / 2.0)
case .right, .rightMirrored:
transform = transform.translatedBy(x: 0, y: size.height)
transform = transform.rotated(by: .pi / -2.0)
case .up, .upMirrored:
break
#if compiler(>=5)
@unknown default:
break
#endif
}
//Flip image one more time if needed to, this is to prevent flipped image
switch orientation {
case .upMirrored, .downMirrored:
transform = transform.translatedBy(x: size.width, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .leftMirrored, .rightMirrored:
transform = transform.translatedBy(x: size.height, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .up, .down, .left, .right:
break
#if compiler(>=5)
@unknown default:
break
#endif
}
context.concatenate(transform)
switch orientation {
case .left, .leftMirrored, .right, .rightMirrored:
context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
default:
context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
}
}
#endif
}
// MARK: - Image Representation
extension KingfisherWrapper where Base: KFCrossPlatformImage {
/// Returns PNG representation of `base` image.
///
/// - Returns: PNG data of image.
public func pngRepresentation() -> Data? {
#if os(macOS)
guard let cgImage = cgImage else {
return nil
}
let rep = NSBitmapImageRep(cgImage: cgImage)
return rep.representation(using: .png, properties: [:])
#else
#if swift(>=4.2)
return base.pngData()
#else
return UIImagePNGRepresentation(base)
#endif
#endif
}
/// Returns JPEG representation of `base` image.
///
/// - Parameter compressionQuality: The compression quality when converting image to JPEG data.
/// - Returns: JPEG data of image.
public func jpegRepresentation(compressionQuality: CGFloat) -> Data? {
#if os(macOS)
guard let cgImage = cgImage else {
return nil
}
let rep = NSBitmapImageRep(cgImage: cgImage)
return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality])
#else
#if swift(>=4.2)
return base.jpegData(compressionQuality: compressionQuality)
#else
return UIImageJPEGRepresentation(base, compressionQuality)
#endif
#endif
}
/// Returns GIF representation of `base` image.
///
/// - Returns: Original GIF data of image.
public func gifRepresentation() -> Data? {
return animatedImageData
}
/// Returns a data representation for `base` image, with the `format` as the format indicator.
///
/// - Parameter format: The format in which the output data should be. If `unknown`, the `base` image will be
/// converted in the PNG representation.
///
/// - Returns: The output data representing.
/// Returns a data representation for `base` image, with the `format` as the format indicator.
/// - Parameters:
/// - format: The format in which the output data should be. If `unknown`, the `base` image will be
/// converted in the PNG representation.
/// - compressionQuality: The compression quality when converting image to a lossy format data.
public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? {
return autoreleasepool { () -> Data? in
let data: Data?
switch format {
case .PNG: data = pngRepresentation()
case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality)
case .GIF: data = gifRepresentation()
case .unknown: data = normalized.kf.pngRepresentation()
}
return data
}
}
}
// MARK: - Creating Images
extension KingfisherWrapper where Base: KFCrossPlatformImage {
/// Creates an animated image from a given data and options. Currently only GIF data is supported.
///
/// - Parameters:
/// - data: The animated image data.
/// - options: Options to use when creating the animated image.
/// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a
/// certain duration. `nil` if anything wrong when creating animated image.
public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
let info: [String: Any] = [
kCGImageSourceShouldCache as String: true,
kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
]
guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
return nil
}
#if os(macOS)
guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
return nil
}
var image: KFCrossPlatformImage?
if options.onlyFirstFrame {
image = animatedImage.images.first
} else {
image = KFCrossPlatformImage(data: data)
var kf = image?.kf
kf?.images = animatedImage.images
kf?.duration = animatedImage.duration
}
image?.kf.animatedImageData = data
return image
#else
var image: KFCrossPlatformImage?
if options.preloadAll || options.onlyFirstFrame {
// Use `images` image if you want to preload all animated data
guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
return nil
}
if options.onlyFirstFrame {
image = animatedImage.images.first
} else {
let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
image = .animatedImage(with: animatedImage.images, duration: duration)
}
image?.kf.animatedImageData = data
} else {
image = KFCrossPlatformImage(data: data, scale: options.scale)
var kf = image?.kf
kf?.imageSource = imageSource
kf?.animatedImageData = data
}
return image
#endif
}
/// Creates an image from a given data and options. `.JPEG`, `.PNG` or `.GIF` is supported. For other
/// image format, image initializer from system will be used. If no image object could be created from
/// the given `data`, `nil` will be returned.
///
/// - Parameters:
/// - data: The image data representation.
/// - options: Options to use when creating the image.
/// - Returns: An `Image` object represents the image if created. If the `data` is invalid or not supported, `nil`
/// will be returned.
public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
var image: KFCrossPlatformImage?
switch data.kf.imageFormat {
case .JPEG:
image = KFCrossPlatformImage(data: data, scale: options.scale)
case .PNG:
image = KFCrossPlatformImage(data: data, scale: options.scale)
case .GIF:
image = KingfisherWrapper.animatedImage(data: data, options: options)
case .unknown:
image = KFCrossPlatformImage(data: data, scale: options.scale)
}
return image
}
/// Creates a downsampled image from given data to a certain size and scale.
///
/// - Parameters:
/// - data: The image data contains a JPEG or PNG image.
/// - pointSize: The target size in point to which the image should be downsampled.
/// - scale: The scale of result image.
/// - Returns: A downsampled `Image` object following the input conditions.
///
/// - Note:
/// Different from image `resize` methods, downsampling will not render the original
/// input image in pixel format. It does downsampling from the image data, so it is much
/// more memory efficient and friendly. Choose to use downsampling as possible as you can.
///
/// The input size should be smaller than the size of input image. If it is larger than the
/// original image size, the result image will be the same size of input without downsampling.
public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
return nil
}
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil)
}
}