177 lines
6.3 KiB
Swift
177 lines
6.3 KiB
Swift
import Foundation
|
|
import AppKit
|
|
import SDWebImageAVIFCoder
|
|
import SDWebImageWebPCoder
|
|
|
|
final class ImageGenerator {
|
|
|
|
private let storage: Storage
|
|
|
|
private let settings: Settings
|
|
|
|
init(storage: Storage, settings: Settings) {
|
|
self.storage = storage
|
|
self.settings = settings
|
|
}
|
|
|
|
private var outputFolder: String {
|
|
settings.paths.imagesOutputFolderPath
|
|
}
|
|
|
|
private var avifCommands: Set<String> = []
|
|
|
|
/**
|
|
Write a file to the output folder containing a script to generate all missing AVIF images.
|
|
|
|
- Note: AVIF images could be generated internally, but the process is very slow.
|
|
*/
|
|
func writeAvifCommandScript() {
|
|
guard !avifCommands.isEmpty else {
|
|
if storage.hasFileInOutputFolder("generate-images.sh") {
|
|
storage.deleteInOutputFolder("generate-images.sh")
|
|
}
|
|
return
|
|
}
|
|
let content = avifCommands.sorted().joined(separator: "\n")
|
|
storage.write(content, to: "generate-images.sh")
|
|
}
|
|
|
|
private func needsToGenerate(_ version: ImageVersion) -> Bool {
|
|
if version.wasPreviouslyGenerated {
|
|
return false
|
|
}
|
|
if exists(version) {
|
|
// Mark as already generated
|
|
version.wasNowGenerated()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MARK: Files
|
|
|
|
private func exists(_ version: ImageVersion) -> Bool {
|
|
storage.hasFileInOutputFolder(version.outputPath)
|
|
}
|
|
|
|
private func write(imageData data: Data, of version: ImageVersion) -> Bool {
|
|
return storage.write(data, to: version.outputPath)
|
|
}
|
|
|
|
// MARK: Image operations
|
|
|
|
func generate(version: ImageVersion) -> Bool {
|
|
guard needsToGenerate(version) else {
|
|
return true
|
|
}
|
|
|
|
guard let data = version.image.dataContent() else {
|
|
print("ImageGenerator: Failed to load data for image \(version.image.id)")
|
|
return false
|
|
}
|
|
|
|
guard let originalImage = NSImage(data: data) else {
|
|
print("ImageGenerator: Failed to load image \(version.image.id)")
|
|
return false
|
|
}
|
|
|
|
let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight))
|
|
|
|
guard let data = create(image: representation, type: version.type, quality: version.quality) else {
|
|
print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)")
|
|
return false
|
|
}
|
|
|
|
if version.type == .avif {
|
|
if version.image.type == .gif {
|
|
// Skip GIFs, since they can't be converted by avifenc
|
|
return true
|
|
}
|
|
// AVIF conversion is very slow, so we save bash commands
|
|
// for the conversion instead
|
|
let baseVersion = ImageVersion(
|
|
image: version.image,
|
|
type: version.image.type,
|
|
maximumWidth: version.maximumWidth,
|
|
maximumHeight: version.maximumHeight)
|
|
let originalImagePath = storage.outputPath(to: baseVersion.outputPath)!.path()
|
|
let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
|
|
let quality = Int(version.quality * 100)
|
|
|
|
avifCommands.insert("avifenc -q \(quality) '\(originalImagePath)' '\(generatedImagePath)'")
|
|
// hasNowGenerated(version)
|
|
return true
|
|
}
|
|
|
|
guard write(imageData: data, of: version) else {
|
|
return false
|
|
}
|
|
version.wasNowGenerated()
|
|
return true
|
|
}
|
|
|
|
private func create(image originalImage: NSImage, width: CGFloat, height: CGFloat) -> NSBitmapImageRep {
|
|
let sourceRep = originalImage.representations[0]
|
|
let sourceSize = NSSize(width: sourceRep.pixelsWide, height: sourceRep.pixelsHigh)
|
|
let maximumSize = NSSize(width: width, height: height)
|
|
let destinationSize = sourceSize.scaledToFit(in: maximumSize)
|
|
|
|
// create NSBitmapRep manually, if using cgImage, the resulting size is wrong
|
|
let representation = NSBitmapImageRep(
|
|
bitmapDataPlanes: nil,
|
|
pixelsWide: Int(destinationSize.width),
|
|
pixelsHigh: Int(destinationSize.height),
|
|
bitsPerSample: 8,
|
|
samplesPerPixel: 4,
|
|
hasAlpha: true,
|
|
isPlanar: false,
|
|
colorSpaceName: NSColorSpaceName.deviceRGB,
|
|
bytesPerRow: Int(destinationSize.width) * 4,
|
|
bitsPerPixel: 32)!
|
|
|
|
let ctx = NSGraphicsContext(bitmapImageRep: representation)
|
|
NSGraphicsContext.saveGraphicsState()
|
|
NSGraphicsContext.current = ctx
|
|
originalImage.draw(in: NSMakeRect(0, 0, destinationSize.width, destinationSize.height))
|
|
ctx?.flushGraphics()
|
|
NSGraphicsContext.restoreGraphicsState()
|
|
return representation
|
|
}
|
|
|
|
// MARK: Avif images
|
|
|
|
private func create(image: NSBitmapImageRep, type: FileType, quality: CGFloat) -> Data? {
|
|
switch type {
|
|
case .jpg:
|
|
return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)])
|
|
case .png:
|
|
return image.representation(using: .png, properties: [.compressionFactor: NSNumber(value: 0.6)])
|
|
case .avif:
|
|
return createAvif(image: image, quality: 0.7)
|
|
case .webp:
|
|
return createWebp(image: image, quality: 0.8)
|
|
case .gif:
|
|
return image.representation(using: .gif, properties: [.compressionFactor: NSNumber(value: quality)])
|
|
case .svg:
|
|
return nil
|
|
case .tiff:
|
|
return nil
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
|
return Data()
|
|
// let newImage = NSImage(size: image.size)
|
|
// newImage.addRepresentation(image)
|
|
// return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
|
|
}
|
|
|
|
private func createWebp(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
|
let newImage = NSImage(size: image.size)
|
|
newImage.addRepresentation(image)
|
|
return SDImageWebPCoder.shared.encodedData(with: newImage, format: .webP, options: [.encodeCompressionQuality: quality])
|
|
}
|
|
}
|