Initial commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// ImageExporter.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// Port of Greenshot's ImageIO: encoding a capture to the configured output format
|
||||
// and writing it to the configured directory using the filename pattern.
|
||||
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum ImageExporter {
|
||||
|
||||
static func data(from image: CGImage, format: OutputFormat, jpegQuality: Int) -> Data? {
|
||||
let rep = NSBitmapImageRep(cgImage: image)
|
||||
rep.size = CGSize(width: image.width, height: image.height)
|
||||
|
||||
var properties: [NSBitmapImageRep.PropertyKey: Any] = [:]
|
||||
if format == .jpg {
|
||||
properties[.compressionFactor] = NSNumber(value: Double(jpegQuality) / 100.0)
|
||||
}
|
||||
return rep.representation(using: format.fileType, properties: properties)
|
||||
}
|
||||
|
||||
/// Flattens an image onto white, needed for formats without an alpha channel.
|
||||
static func flattened(_ image: CGImage) -> CGImage {
|
||||
guard let context = CGContext(data: nil,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 0,
|
||||
space: CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return image }
|
||||
context.setFillColor(NSColor.white.cgColor)
|
||||
context.fill(CGRect(x: 0, y: 0, width: image.width, height: image.height))
|
||||
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
|
||||
return context.makeImage() ?? image
|
||||
}
|
||||
|
||||
/// The URL a capture would be saved to with the current configuration.
|
||||
static func suggestedURL(for details: CaptureDetails,
|
||||
format: OutputFormat = Configuration.shared.outputFileFormat) -> URL {
|
||||
let configuration = Configuration.shared
|
||||
let name = FilenamePattern.filename(pattern: configuration.outputFileFilenamePattern,
|
||||
details: details,
|
||||
format: format)
|
||||
let directory = URL(fileURLWithPath: configuration.outputFilePath, isDirectory: true)
|
||||
return uniqueURL(directory.appendingPathComponent(name))
|
||||
}
|
||||
|
||||
/// Appends " (2)", " (3)", … until the URL is free.
|
||||
static func uniqueURL(_ url: URL) -> URL {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else { return url }
|
||||
let directory = url.deletingLastPathComponent()
|
||||
let ext = url.pathExtension
|
||||
let base = url.deletingPathExtension().lastPathComponent
|
||||
var index = 2
|
||||
while index < 1000 {
|
||||
let candidate = directory
|
||||
.appendingPathComponent("\(base) (\(index))")
|
||||
.appendingPathExtension(ext)
|
||||
if !FileManager.default.fileExists(atPath: candidate.path) { return candidate }
|
||||
index += 1
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func write(_ image: CGImage, to url: URL, format: OutputFormat) throws -> URL {
|
||||
let configuration = Configuration.shared
|
||||
let source = (format == .jpg || format == .bmp) ? flattened(image) : image
|
||||
guard let data = data(from: source, format: format, jpegQuality: configuration.outputFileJpegQuality) else {
|
||||
throw CaptureError.captureFailed("Could not encode the image as \(format.displayName).")
|
||||
}
|
||||
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: .atomic)
|
||||
return url
|
||||
}
|
||||
|
||||
static func format(forPathExtension pathExtension: String) -> OutputFormat {
|
||||
switch pathExtension.lowercased() {
|
||||
case "jpg", "jpeg": return .jpg
|
||||
case "tif", "tiff": return .tiff
|
||||
case "bmp": return .bmp
|
||||
case "gif": return .gif
|
||||
default: return .png
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user