NetNewsWire/Core/Sources/Core/RSImage.swift
2024-03-22 11:35:04 -07:00

226 lines
6.4 KiB
Swift

//
// RSImage.swift
// RSCore
//
// Created by Maurice Parker on 4/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
#if os(macOS)
import AppKit
public typealias RSImage = NSImage
#endif
#if os(iOS)
import UIKit
public typealias RSImage = UIImage
#endif
public extension RSImage {
#if os(macOS)
static let maxScreenScale = CGFloat(2)
#endif
#if os(iOS)
static let maxScreenScale = CGFloat(3)
#endif
/// Create a colored image from the source image using a specified color.
///
/// - Parameter color: The color with which to fill the mask image.
/// - Returns: A new masked image.
func maskWithColor(color: CGColor) -> RSImage? {
#if os(macOS)
guard let maskImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
#else
guard let maskImage = cgImage else { return nil }
#endif
let width = size.width
let height = size.height
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)!
context.clip(to: bounds, mask: maskImage)
context.setFillColor(color)
context.fill(bounds)
if let cgImage = context.makeImage() {
#if os(macOS)
let coloredImage = RSImage(cgImage: cgImage, size: CGSize(width: cgImage.width, height: cgImage.height))
#else
let coloredImage = RSImage(cgImage: cgImage)
#endif
return coloredImage
} else {
return nil
}
}
#if os(iOS)
/// Tint an image.
///
/// - Parameter color: The color to use to tint the image.
/// - Returns: The tinted image.
func tinted(color: UIColor) -> UIImage? {
let image = withRenderingMode(.alwaysTemplate)
let imageView = UIImageView(image: image)
imageView.tintColor = color
UIGraphicsBeginImageContextWithOptions(image.size, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
imageView.layer.render(in: context)
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return tintedImage
} else {
return self
}
}
#endif
/// Returns a data representation of the image.
///
/// The resultant data is TIFF data on macOS, and PNG data on iOS.
/// - Returns: Data representing the image.
func dataRepresentation() -> Data? {
#if os(macOS)
return tiffRepresentation
#else
return pngData()
#endif
}
/// Asynchronously initializes an image from data.
///
/// - Parameters:
/// - data: The data object containing the image data.
/// - imageResultBlock: The closure to call when the image has been initialized.
static func image(with data: Data, imageResultBlock: @escaping ImageResultBlock) {
DispatchQueue.global().async {
let image = RSImage(data: data)
DispatchQueue.main.async {
imageResultBlock(image)
}
}
}
/// Create a scaled image from image data.
///
/// - Note: the returned image may be larger than `maxPixelSize`, but not more than `maxPixelSize * 2`.
/// - Parameters:
/// - data: The data object containing the image data.
/// - maxPixelSize: The maximum dimension of the image.
static func scaleImage(_ data: Data, maxPixelSize: Int) -> CGImage? {
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
return nil
}
let numberOfImages = CGImageSourceGetCount(imageSource)
// If the image size matches exactly, then return it.
for i in 0..<numberOfImages {
guard let cfImageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) else {
continue
}
let imageProperties = cfImageProperties as NSDictionary
guard let imagePixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? NSNumber else {
continue
}
if imagePixelWidth.intValue != maxPixelSize {
continue
}
guard let imagePixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? NSNumber else {
continue
}
if imagePixelHeight.intValue != maxPixelSize {
continue
}
return CGImageSourceCreateImageAtIndex(imageSource, i, nil)
}
// If image height > maxPixelSize, but <= maxPixelSize * 2, then return it.
for i in 0..<numberOfImages {
guard let cfImageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) else {
continue
}
let imageProperties = cfImageProperties as NSDictionary
guard let imagePixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? NSNumber else {
continue
}
if imagePixelWidth.intValue > maxPixelSize * 2 || imagePixelWidth.intValue < maxPixelSize {
continue
}
guard let imagePixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? NSNumber else {
continue
}
if imagePixelHeight.intValue > maxPixelSize * 2 || imagePixelHeight.intValue < maxPixelSize {
continue
}
return CGImageSourceCreateImageAtIndex(imageSource, i, nil)
}
// If the image data contains a smaller image than the max size, just return it.
for i in 0..<numberOfImages {
guard let cfImageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) else {
continue
}
let imageProperties = cfImageProperties as NSDictionary
guard let imagePixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? NSNumber else {
continue
}
if imagePixelWidth.intValue < 1 || imagePixelWidth.intValue > maxPixelSize {
continue
}
guard let imagePixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? NSNumber else {
continue
}
if imagePixelHeight.intValue > 0 && imagePixelHeight.intValue <= maxPixelSize {
if let image = CGImageSourceCreateImageAtIndex(imageSource, i, nil) {
return image
}
}
}
return RSImage.createThumbnail(imageSource, maxPixelSize: maxPixelSize)
}
/// Create a thumbnail from a CGImageSource.
///
/// - Parameters:
/// - imageSource: The `CGImageSource` from which to create the thumbnail.
/// - maxPixelSize: The maximum dimension of the resulting image.
static func createThumbnail(_ imageSource: CGImageSource, maxPixelSize: Int) -> CGImage? {
let options = [kCGImageSourceCreateThumbnailWithTransform : true,
kCGImageSourceCreateThumbnailFromImageIfAbsent : true,
kCGImageSourceThumbnailMaxPixelSize : NSNumber(value: maxPixelSize)]
return CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
}
}