86 lines
3.1 KiB
Swift
86 lines
3.1 KiB
Swift
import Foundation
|
|
import AppKit // NSImage
|
|
private import CoreUI // CUICatalog
|
|
private import os // OSLog
|
|
|
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "AssetCarReader")
|
|
|
|
public class CarReader {
|
|
private let catalog: CUICatalog
|
|
|
|
public init?(_ data: Data) {
|
|
do {
|
|
catalog = try data.withUnsafeBytes { try CUICatalog(bytes: $0.baseAddress!, length: UInt64(data.count)) }
|
|
} catch {
|
|
os_log(.error, log: log, "[asset-car] ERROR: could not open catalog: %{public}@", error.localizedDescription)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Use `CUICatalog` to extract an image from `Assets.car`
|
|
public func imageFromAssetsCar(_ imageName: String) -> NSImage? {
|
|
if let validName = verifyNameExists(imageName, in: catalog) {
|
|
if let bestImage = findHighestResolutionIcon(catalog.images(withName: validName)) {
|
|
os_log(.debug, log: log, "[asset-car] using Assets.car with key %{public}@", validName)
|
|
return NSImage(cgImage: bestImage.image, size: bestImage.size)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
// MARK: - Private methods
|
|
|
|
/// Helper method to check available icon names. Will return a valid name or `nil` if no image with that key is found.
|
|
private func verifyNameExists(_ imageName: String, in catalog: CUICatalog) -> String? {
|
|
if let availableNames = catalog.allImageNames(), !availableNames.contains(imageName) {
|
|
// Theoretically this should never happen. Assuming the image name is found in an image file.
|
|
os_log(.info, log: log, "[asset-car] WARN: key '%{public}@' does not match any available key", imageName)
|
|
|
|
if let alternativeName = searchAlternativeName(imageName, inAvailable: availableNames) {
|
|
os_log(.info, log: log, "[asset-car] falling back to '%{public}@'", alternativeName)
|
|
return alternativeName
|
|
}
|
|
os_log(.debug, log: log, "[asset-car] available keys: %{public}@", catalog.allImageNames() ?? [])
|
|
return nil
|
|
}
|
|
return imageName
|
|
}
|
|
|
|
/// If exact name does not exist in catalog, search for a name that shares the same prefix.
|
|
/// E.g., "AppIcon60x60" may match "AppIcon" or "AppIcon60x60_small"
|
|
private func searchAlternativeName(_ originalName: String, inAvailable availableNames: [String]) -> String? {
|
|
var bestOption: String? = nil
|
|
var bestDiff: Int = 999
|
|
|
|
for option in availableNames {
|
|
if option.hasPrefix(originalName) || originalName.hasPrefix(option) {
|
|
let thisDiff = max(originalName.count, option.count) - min(originalName.count, option.count)
|
|
if thisDiff < bestDiff {
|
|
bestDiff = thisDiff
|
|
bestOption = option
|
|
}
|
|
}
|
|
}
|
|
return bestOption
|
|
}
|
|
|
|
/// Given a list of `CUINamedImage`, return the one with the highest resolution. Vector graphics are ignored.
|
|
private func findHighestResolutionIcon(_ availableImages: [CUINamedImage]) -> CUINamedImage? {
|
|
var largestWidth: CGFloat = 0
|
|
var largestImage: CUINamedImage? = nil
|
|
// cast to NSArray is necessary as otherwise this will crash
|
|
for img in availableImages as NSArray {
|
|
guard let img = img as? CUINamedImage else {
|
|
continue // ignore CUINamedMultisizeImageSet
|
|
}
|
|
let w = img.size.width
|
|
if w > largestWidth {
|
|
largestWidth = w
|
|
largestImage = img
|
|
}
|
|
}
|
|
return largestImage
|
|
}
|
|
}
|